[SECURITY] Filter disallowed properties in form editor
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Mvc / Property / TypeConverter / FormDefinitionArrayConverter.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Form\Mvc\Property\TypeConverter;
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\Authentication\BackendUserAuthentication;
19 use TYPO3\CMS\Core\Utility\ArrayUtility;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
22 use TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter;
23 use TYPO3\CMS\Form\Domain\Configuration\ConfigurationService;
24 use TYPO3\CMS\Form\Domain\Configuration\Exception\PropertyException;
25 use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService;
26 use TYPO3\CMS\Form\Domain\Configuration\FormDefinitionValidationService;
27 use TYPO3\CMS\Form\Type\FormDefinitionArray;
28
29 /**
30 * Converter for form definition arrays
31 *
32 * @internal
33 */
34 class FormDefinitionArrayConverter extends AbstractTypeConverter
35 {
36 /**
37 * @var array<string>
38 */
39 protected $sourceTypes = ['string'];
40
41 /**
42 * @var string
43 */
44 protected $targetType = FormDefinitionArray::class;
45
46 /**
47 * @var int
48 */
49 protected $priority = 10;
50
51 /**
52 * @var ConfigurationService
53 */
54 protected $configurationService;
55
56 /**
57 * Convert from $source to $targetType, a noop if the source is an array.
58 * If it is an empty string it will be converted to an empty array.
59 *
60 * @param string $source
61 * @param string $targetType
62 * @param array $convertedChildProperties
63 * @param PropertyMappingConfigurationInterface $configuration
64 * @return FormDefinitionArray
65 * @throws PropertyException
66 */
67 public function convertFrom($source, $targetType, array $convertedChildProperties = [], PropertyMappingConfigurationInterface $configuration = null)
68 {
69 $rawFormDefinitionArray = json_decode($source, true);
70
71 if (json_last_error() !== JSON_ERROR_NONE) {
72 throw new PropertyException('Unable to decode JSON source: ' . json_last_error_msg(), 1512578002);
73 }
74
75 $formDefinitionValidationService = $this->getFormDefinitionValidationService();
76 $formDefinitionConversionService = $this->getFormDefinitionConversionService();
77
78 // Extend the hmac hashing key with the "per form editor session (load / save)" unique key.
79 // @see \TYPO3\CMS\Form\Domain\Configuration\FormDefinitionConversionService::addHmacData
80 $sessionToken = $this->retrieveSessionToken();
81
82 $prototypeName = $rawFormDefinitionArray['prototypeName'] ?? null;
83 $identifier = $rawFormDefinitionArray['identifier'] ?? null;
84
85 // A modification of the properties "prototypeName" and "identifier" from the root form element
86 // through the form editor is always forbidden.
87 try {
88 if (!$formDefinitionValidationService->isPropertyValueEqualToHistoricalValue([$identifier, 'identifier'], $identifier, $rawFormDefinitionArray['_orig_identifier'] ?? [], $sessionToken)) {
89 throw new PropertyException('Unauthorized modification of "identifier".', 1528538324);
90 }
91
92 if (!$formDefinitionValidationService->isPropertyValueEqualToHistoricalValue([$identifier, 'prototypeName'], $prototypeName, $rawFormDefinitionArray['_orig_prototypeName'] ?? [], $sessionToken)) {
93 throw new PropertyException('Unauthorized modification of "prototype name".', 1528538323);
94 }
95 } catch (PropertyException $e) {
96 throw new PropertyException('Unauthorized modification of "prototype name" or "identifier".', 1528538322);
97 }
98
99 $formDefinitionValidationService->validateFormDefinitionProperties($rawFormDefinitionArray, $prototypeName, $sessionToken);
100
101 // @todo move all the transformations to FormDefinitionConversionService
102 $rawFormDefinitionArray = $this->filterEmptyArrays($rawFormDefinitionArray);
103 $rawFormDefinitionArray = $this->transformMultiValueElementsForFormFramework($rawFormDefinitionArray);
104 // @todo: replace with rte parsing
105 $rawFormDefinitionArray = ArrayUtility::stripTagsFromValuesRecursive($rawFormDefinitionArray);
106 $rawFormDefinitionArray = $formDefinitionConversionService->removeHmacData($rawFormDefinitionArray);
107
108 $formDefinitionArray = GeneralUtility::makeInstance(FormDefinitionArray::class, $rawFormDefinitionArray);
109 return $formDefinitionArray;
110 }
111
112 /**
113 * Some data which is build by the form editor needs a transformation before
114 * it can be used by the framework.
115 * Multivalue elements like select elements produce data like:
116 *
117 * [
118 * _label => 'label'
119 * _value => 'value'
120 * ]
121 *
122 * This method transforms this into:
123 *
124 * [
125 * 'value' => 'label'
126 * ]
127 *
128 * @param array $input
129 * @return array
130 */
131 protected function transformMultiValueElementsForFormFramework(array $input): array
132 {
133 $output = [];
134
135 foreach ($input as $key => $value) {
136 if (is_int($key) && is_array($value) && isset($value['_label']) && isset($value['_value'])) {
137 $key = $value['_value'];
138 $value = $value['_label'];
139 }
140
141 if (is_array($value)) {
142 $output[$key] = $this->transformMultiValueElementsForFormFramework($value);
143 } else {
144 $output[$key] = $value;
145 }
146 }
147
148 return $output;
149 }
150
151 /**
152 * Remove keys from an array if the key value is an empty array
153 *
154 * @todo ArrayUtility?
155 * @param array $array
156 * @return array
157 */
158 protected function filterEmptyArrays(array $array): array
159 {
160 foreach ($array as $key => $value) {
161 if (!is_array($value)) {
162 continue;
163 }
164 if (empty($value)) {
165 unset($array[$key]);
166 continue;
167 }
168 $array[$key] = $this->filterEmptyArrays($value);
169 if (empty($array[$key])) {
170 unset($array[$key]);
171 }
172 }
173
174 return $array;
175 }
176
177 /**
178 * @return string
179 */
180 protected function retrieveSessionToken(): string
181 {
182 return $this->getBackendUser()->getSessionData('extFormProtectionSessionToken');
183 }
184
185 /**
186 * @return FormDefinitionValidationService
187 */
188 protected function getFormDefinitionValidationService(): FormDefinitionValidationService
189 {
190 return GeneralUtility::makeInstance(FormDefinitionValidationService::class);
191 }
192
193 /**
194 * @return FormDefinitionConversionService
195 */
196 protected function getFormDefinitionConversionService(): FormDefinitionConversionService
197 {
198 return GeneralUtility::makeInstance(FormDefinitionConversionService::class);
199 }
200
201 /**
202 * @return BackendUserAuthentication
203 */
204 protected function getBackendUser(): BackendUserAuthentication
205 {
206 return $GLOBALS['BE_USER'];
207 }
208 }