[TASK] Add spaces around '=' of 'strict_types=1'
[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 if ($form['invalid']) {
114 $dataStructure['sheets']['sDEF']['ROOT']['el']['settings.persistenceIdentifier']['TCEforms']['config']['items'][] = [
115 $form['name'] . ' (' . $form['persistenceIdentifier'] . ')',
116 $form['persistenceIdentifier'],
117 'overlay-missing'
118 ];
119 } else {
120 $dataStructure['sheets']['sDEF']['ROOT']['el']['settings.persistenceIdentifier']['TCEforms']['config']['items'][] = [
121 $form['name'] . ' (' . $form['persistenceIdentifier'] . ')',
122 $form['persistenceIdentifier'],
123 'content-form'
124 ];
125 }
126 }
127
128 if (!empty($identifier['ext-form-persistenceIdentifier']) && !$formIsAccessible) {
129 $dataStructure['sheets']['sDEF']['ROOT']['el']['settings.persistenceIdentifier']['TCEforms']['config']['items'][] = [
130 sprintf(
131 $this->getLanguageService()->sL(self::L10N_PREFIX . 'tt_content.preview.inaccessiblePersistenceIdentifier'),
132 $identifier['ext-form-persistenceIdentifier']
133 ),
134 $identifier['ext-form-persistenceIdentifier'],
135 ];
136 }
137
138 // If a specific form is selected and if finisher override is active, add finisher sheets
139 if (!empty($identifier['ext-form-persistenceIdentifier'])
140 && $formIsAccessible
141 && isset($identifier['ext-form-overrideFinishers'])
142 && $identifier['ext-form-overrideFinishers'] === true
143 ) {
144 $persistenceIdentifier = $identifier['ext-form-persistenceIdentifier'];
145 $formDefinition = $formPersistenceManager->load($persistenceIdentifier);
146 $newSheets = $this->getAdditionalFinisherSheets($persistenceIdentifier, $formDefinition);
147 ArrayUtility::mergeRecursiveWithOverrule(
148 $dataStructure,
149 $newSheets
150 );
151 }
152 } catch (NoSuchFileException $e) {
153 $dataStructure = $this->addSelectedPersistenceIdentifier($identifier['ext-form-persistenceIdentifier'], $dataStructure);
154 $this->addInvalidFrameworkConfigurationFlashMessage($e);
155 } catch (ParseErrorException $e) {
156 $dataStructure = $this->addSelectedPersistenceIdentifier($identifier['ext-form-persistenceIdentifier'], $dataStructure);
157 $this->addInvalidFrameworkConfigurationFlashMessage($e);
158 }
159 }
160 return $dataStructure;
161 }
162
163 /**
164 * Returns additional flexform sheets with finisher fields
165 *
166 * @param string $persistenceIdentifier Current persistence identifier
167 * @param array $formDefinition The form definition
168 * @return array
169 */
170 protected function getAdditionalFinisherSheets(string $persistenceIdentifier, array $formDefinition): array
171 {
172 if (!isset($formDefinition['finishers']) || empty($formDefinition['finishers'])) {
173 return [];
174 }
175
176 $prototypeName = $formDefinition['prototypeName'] ?? 'standard';
177 $prototypeConfiguration = GeneralUtility::makeInstance(ObjectManager::class)
178 ->get(ConfigurationService::class)
179 ->getPrototypeConfiguration($prototypeName);
180
181 if (!isset($prototypeConfiguration['finishersDefinition']) || empty($prototypeConfiguration['finishersDefinition'])) {
182 return [];
183 }
184
185 $formIdentifier = $formDefinition['identifier'];
186 $finishersDefinition = $prototypeConfiguration['finishersDefinition'];
187
188 $sheets = ['sheets' => []];
189 foreach ($formDefinition['finishers'] as $finisherValue) {
190 $finisherIdentifier = $finisherValue['identifier'];
191 if (!isset($finishersDefinition[$finisherIdentifier]['FormEngine']['elements'])) {
192 continue;
193 }
194 $sheetIdentifier = md5(
195 implode('', [
196 $persistenceIdentifier,
197 $prototypeName,
198 $formIdentifier,
199 $finisherIdentifier
200 ])
201 );
202
203 if (isset($finishersDefinition[$finisherIdentifier]['FormEngine']['translationFile'])) {
204 $translationFile = $finishersDefinition[$finisherIdentifier]['FormEngine']['translationFile'];
205 } else {
206 $translationFile = $prototypeConfiguration['formEngine']['translationFile'];
207 }
208
209 $finishersDefinition[$finisherIdentifier]['FormEngine'] = TranslationService::getInstance()->translateValuesRecursive(
210 $finishersDefinition[$finisherIdentifier]['FormEngine'],
211 $translationFile
212 );
213 $finisherLabel = $finishersDefinition[$finisherIdentifier]['FormEngine']['label'];
214 $sheet = $this->initializeNewSheetArray($sheetIdentifier, $finisherLabel);
215
216 $sheetElements = [];
217 foreach ($finisherValue['options'] as $optionKey => $optionValue) {
218 if (is_array($optionValue)) {
219 $optionKey = $optionKey . '.' . $this->implodeArrayKeys($finisherValue['options'][$optionKey]);
220 try {
221 $elementConfiguration = ArrayUtility::getValueByPath(
222 $finishersDefinition[$finisherIdentifier]['FormEngine']['elements'],
223 $optionKey,
224 '.'
225 );
226 } catch (\RuntimeException $exception) {
227 $elementConfiguration = null;
228 }
229 try {
230 $optionValue = ArrayUtility::getValueByPath($finisherValue['options'], $optionKey, '.');
231 } catch (\RuntimeException $exception) {
232 $optionValue = null;
233 }
234 } else {
235 $elementConfiguration = $finishersDefinition[$finisherIdentifier]['FormEngine']['elements'][$optionKey];
236 }
237
238 if (empty($elementConfiguration)) {
239 continue;
240 }
241
242 if (empty($optionValue)) {
243 $elementConfiguration['label'] .= ' (default: "[Empty]")';
244 } else {
245 $elementConfiguration['label'] .= ' (default: "' . $optionValue . '")';
246 }
247 $elementConfiguration['config']['default'] = $optionValue;
248 $sheetElements['settings.finishers.' . $finisherIdentifier . '.' . $optionKey] = $elementConfiguration;
249 }
250
251 ksort($sheetElements);
252
253 $sheet[$sheetIdentifier]['ROOT']['el'] = $sheetElements;
254 ArrayUtility::mergeRecursiveWithOverrule($sheets['sheets'], $sheet);
255 }
256 if (empty($sheets['sheets'])) {
257 return [];
258 }
259
260 return $sheets;
261 }
262
263 /**
264 * Boilerplate XML array of a new sheet
265 *
266 * @param string $sheetIdentifier
267 * @param string $finisherName
268 * @throws \InvalidArgumentException
269 * @return array
270 */
271 protected function initializeNewSheetArray(string $sheetIdentifier, string $finisherName): array
272 {
273 if (empty($sheetIdentifier)) {
274 throw new \InvalidArgumentException('$sheetIdentifier must not be empty.', 1472060918);
275 }
276 if (empty($finisherName)) {
277 throw new \InvalidArgumentException('$finisherName must not be empty.', 1472060919);
278 }
279
280 return [
281 $sheetIdentifier => [
282 'ROOT' => [
283 'TCEforms' => [
284 'sheetTitle' => $finisherName,
285 ],
286 'type' => 'array',
287 'el' => [],
288 ],
289 ],
290 ];
291 }
292
293 /**
294 * Recursive helper to implode a nested array to a dotted path notation
295 *
296 * ['a' => [ 'b' => 42 ] ] becomes 'a.b'
297 *
298 * @param array $nestedArray
299 * @return string
300 */
301 protected function implodeArrayKeys(array $nestedArray): string
302 {
303 $dottedPath = (string)key($nestedArray);
304 if (is_array($nestedArray[$dottedPath])) {
305 $dottedPath .= '.' . $this->implodeArrayKeys($nestedArray[$dottedPath]);
306 }
307 return $dottedPath;
308 }
309
310 /**
311 * @param string $persistenceIdentifier
312 * @param array $dataStructure
313 * @return array
314 */
315 protected function addSelectedPersistenceIdentifier(string $persistenceIdentifier, array $dataStructure): array
316 {
317 if (!empty($persistenceIdentifier)) {
318 $dataStructure['sheets']['sDEF']['ROOT']['el']['settings.persistenceIdentifier']['TCEforms']['config']['items'][] = [
319 sprintf(
320 $this->getLanguageService()->sL(self::L10N_PREFIX . 'tt_content.preview.inaccessiblePersistenceIdentifier'),
321 $persistenceIdentifier
322 ),
323 $persistenceIdentifier,
324 ];
325 }
326
327 return $dataStructure;
328 }
329
330 /**
331 * @param \Exception $e
332 */
333 protected function addInvalidFrameworkConfigurationFlashMessage(\Exception $e)
334 {
335 $messageText = sprintf(
336 $this->getLanguageService()->sL(self::L10N_PREFIX . 'tt_content.preview.invalidFrameworkConfiguration.text'),
337 $e->getMessage()
338 );
339
340 GeneralUtility::makeInstance(ObjectManager::class)
341 ->get(FlashMessageService::class)
342 ->getMessageQueueByIdentifier('core.template.flashMessages')
343 ->enqueue(
344 GeneralUtility::makeInstance(
345 FlashMessage::class,
346 $messageText,
347 $this->getLanguageService()->sL(self::L10N_PREFIX . 'tt_content.preview.invalidFrameworkConfiguration.title'),
348 AbstractMessage::ERROR,
349 true
350 )
351 );
352 }
353
354 /**
355 * @return LanguageService
356 */
357 protected function getLanguageService(): LanguageService
358 {
359 return $GLOBALS['LANG'];
360 }
361 }