[BUGFIX] Exception in EXT:form due to invalid array lookup
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Mvc / Configuration / InheritancesResolverService.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Form\Mvc\Configuration;
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\Mvc\Configuration\Exception\CycleInheritancesException;
22
23 /**
24 * Resolve declared inheritances within an configuration array
25 *
26 * Scope: frontend / backend
27 * @internal
28 */
29 class InheritancesResolverService
30 {
31
32 /**
33 * The operator which is used to declare inheritances
34 */
35 const INHERITANCE_OPERATOR = '__inheritances';
36
37 /**
38 * The reference configuration is used to get untouched values which
39 * can be merged into the touched configuration.
40 *
41 * @var array
42 */
43 protected $referenceConfiguration = [];
44
45 /**
46 * This stack is needed to find cyclically inheritances which are on
47 * the same nesting level but which do not follow each other directly.
48 *
49 * @var array
50 */
51 protected $inheritanceStack = [];
52
53 /**
54 * Needed to park a configuration path for cyclically inheritances
55 * detection while inheritances for this path is ongoing.
56 *
57 * @var string
58 */
59 protected $inheritancePathToCkeck = '';
60
61 /**
62 * Returns an instance of this service. Additionally the configuration
63 * which should be resolved can be passed.
64 *
65 * @param array $configuration
66 * @return InheritancesResolverService
67 * @internal
68 */
69 public static function create(array $configuration = []): InheritancesResolverService
70 {
71 /** @var InheritancesResolverService $inheritancesResolverService */
72 $inheritancesResolverService = GeneralUtility::makeInstance(ObjectManager::class)
73 ->get(self::class);
74 $inheritancesResolverService->setReferenceConfiguration($configuration);
75 return $inheritancesResolverService;
76 }
77
78 /**
79 * Reset the state of this service.
80 * Mainly introduced for unit tests.
81 *
82 * @return InheritancesResolverService
83 * @internal
84 */
85 public function reset()
86 {
87 $this->referenceConfiguration = [];
88 $this->inheritanceStack = [];
89 $this->inheritancePathToCkeck = '';
90 return $this;
91 }
92
93 /**
94 * Set the reference configuration which is used to get untouched
95 * values which can be merged into the touched configuration.
96 *
97 * @param array
98 * @return InheritancesResolverService
99 */
100 public function setReferenceConfiguration(array $referenceConfiguration)
101 {
102 $this->referenceConfiguration = $referenceConfiguration;
103 return $this;
104 }
105
106 /**
107 * Resolve all inheritances within a configuration.
108 * After that the configuration array is cleaned from the
109 * inheritance operator.
110 *
111 * @return array
112 * @internal
113 */
114 public function getResolvedConfiguration(): array
115 {
116 $configuration = $this->resolve($this->referenceConfiguration);
117 $configuration = $this->removeInheritanceOperatorRecursive($configuration);
118 return $configuration;
119 }
120
121 /**
122 * Resolve all inheritances within a configuration.
123 *
124 * @toDo: More description
125 * @param array $configuration
126 * @param array $pathStack
127 * @param bool $setInheritancePathToCkeck
128 * @return array
129 */
130 protected function resolve(
131 array $configuration,
132 array $pathStack = [],
133 bool $setInheritancePathToCkeck = true
134 ): array {
135 foreach ($configuration as $key => $values) {
136 $pathStack[] = $key;
137 $path = implode('.', $pathStack);
138
139 $this->throwExceptionIfCycleInheritances($path, $path);
140 if ($setInheritancePathToCkeck) {
141 $this->inheritancePathToCkeck = $path;
142 }
143
144 if (is_array($configuration[$key])) {
145 if (isset($configuration[$key][self::INHERITANCE_OPERATOR])) {
146 try {
147 $inheritances = ArrayUtility::getValueByPath(
148 $this->referenceConfiguration,
149 $path . '.' . self::INHERITANCE_OPERATOR,
150 '.'
151 );
152 } catch (\RuntimeException $exception) {
153 $inheritances = null;
154 }
155
156 if (is_array($inheritances)) {
157 $inheritedConfigurations = $this->resolveInheritancesRecursive($inheritances);
158
159 $configuration[$key] = $this->mergeRecursiveWithOverrule(
160 $inheritedConfigurations,
161 $configuration[$key]
162 );
163 }
164
165 unset($configuration[$key][self::INHERITANCE_OPERATOR]);
166 }
167
168 if (!empty($configuration[$key])) {
169 $configuration[$key] = $this->resolve(
170 $configuration[$key],
171 $pathStack
172 );
173 }
174 }
175 array_pop($pathStack);
176 }
177
178 return $configuration;
179 }
180
181 /**
182 * Additional helper for the resolve method.
183 *
184 * @toDo: More description
185 * @param array $inheritances
186 * @return array
187 * @throws CycleInheritancesException
188 */
189 protected function resolveInheritancesRecursive(array $inheritances): array
190 {
191 ksort($inheritances);
192 $inheritedConfigurations = [];
193 foreach ($inheritances as $inheritancePath) {
194 $this->throwExceptionIfCycleInheritances($inheritancePath, $inheritancePath);
195 try {
196 $inheritedConfiguration = ArrayUtility::getValueByPath(
197 $this->referenceConfiguration,
198 $inheritancePath,
199 '.'
200 );
201 } catch (\RuntimeException $exception) {
202 $inheritedConfiguration = null;
203 }
204
205 if (
206 isset($inheritedConfiguration[self::INHERITANCE_OPERATOR])
207 && count($inheritedConfiguration) === 1
208 ) {
209 if ($this->inheritancePathToCkeck === $inheritancePath) {
210 throw new CycleInheritancesException(
211 $this->inheritancePathToCkeck . ' has cycle inheritances',
212 1474900796
213 );
214 }
215
216 $inheritedConfiguration = $this->resolveInheritancesRecursive(
217 $inheritedConfiguration[self::INHERITANCE_OPERATOR]
218 );
219 } else {
220 $pathStack = explode('.', $inheritancePath);
221 $key = array_pop($pathStack);
222 $newConfiguration = [
223 $key => $inheritedConfiguration
224 ];
225 $inheritedConfiguration = $this->resolve(
226 $newConfiguration,
227 $pathStack,
228 false
229 );
230 $inheritedConfiguration = $inheritedConfiguration[$key];
231 }
232
233 $inheritedConfigurations = $this->mergeRecursiveWithOverrule(
234 $inheritedConfigurations,
235 $inheritedConfiguration
236 );
237 }
238
239 return $inheritedConfigurations;
240 }
241
242 /**
243 * Throw an exception if a cycle is detected.
244 *
245 * @toDo: More description
246 * @param string $path
247 * @param string $pathToCheck
248 * @return void
249 * @throws CycleInheritancesException
250 */
251 protected function throwExceptionIfCycleInheritances(string $path, string $pathToCheck)
252 {
253 try {
254 $configuration = ArrayUtility::getValueByPath(
255 $this->referenceConfiguration,
256 $path,
257 '.'
258 );
259 } catch (\RuntimeException $exception) {
260 $configuration = null;
261 }
262
263 if (isset($configuration[self::INHERITANCE_OPERATOR])) {
264 try {
265 $inheritances = ArrayUtility::getValueByPath(
266 $this->referenceConfiguration,
267 $path . '.' . self::INHERITANCE_OPERATOR,
268 '.'
269 );
270 } catch (\RuntimeException $exception) {
271 $inheritances = null;
272 }
273
274 if (is_array($inheritances)) {
275 foreach ($inheritances as $inheritancePath) {
276 try {
277 $configuration = ArrayUtility::getValueByPath(
278 $this->referenceConfiguration,
279 $inheritancePath,
280 '.'
281 );
282 } catch (\RuntimeException $exception) {
283 $configuration = null;
284 }
285
286 if (isset($configuration[self::INHERITANCE_OPERATOR])) {
287 try {
288 $_inheritances = ArrayUtility::getValueByPath(
289 $this->referenceConfiguration,
290 $inheritancePath . '.' . self::INHERITANCE_OPERATOR,
291 '.'
292 );
293 } catch (\RuntimeException $exception) {
294 $_inheritances = null;
295 }
296
297 foreach ($_inheritances as $_inheritancePath) {
298 if (strpos($pathToCheck, $_inheritancePath) === 0) {
299 throw new CycleInheritancesException(
300 $pathToCheck . ' has cycle inheritances',
301 1474900797
302 );
303 }
304 }
305 }
306
307 if (
308 is_array($this->inheritanceStack[$pathToCheck])
309 && in_array($inheritancePath, $this->inheritanceStack[$pathToCheck])
310 ) {
311 $this->inheritanceStack[$pathToCheck][] = $inheritancePath;
312 throw new CycleInheritancesException(
313 $pathToCheck . ' has cycle inheritances',
314 1474900799
315 );
316 }
317 $this->inheritanceStack[$pathToCheck][] = $inheritancePath;
318 $this->throwExceptionIfCycleInheritances($inheritancePath, $pathToCheck);
319 }
320 $this->inheritanceStack[$pathToCheck] = null;
321 }
322 }
323 }
324
325 /**
326 * Recursively remove self::INHERITANCE_OPERATOR keys
327 *
328 * @param array $array
329 * @return array the modified array
330 */
331 protected function removeInheritanceOperatorRecursive(array $array): array
332 {
333 $result = $array;
334 foreach ($result as $key => $value) {
335 if ($key === self::INHERITANCE_OPERATOR) {
336 unset($result[$key]);
337 continue;
338 }
339
340 if (is_array($value)) {
341 $result[$key] = $this->removeInheritanceOperatorRecursive($value);
342 }
343 }
344 return $result;
345 }
346
347 /**
348 * Merges two arrays recursively and "binary safe" (integer keys are overridden as well),
349 * overruling similar values in the first array ($firstArray) with the
350 * values of the second array ($secondArray)
351 * In case of identical keys, ie. keeping the values of the second.
352 * This is basicly the Extbase arrayMergeRecursiveOverrule method.
353 * This method act different to the core mergeRecursiveWithOverrule method.
354 * This method has the possibility to overrule a array value within the
355 * $firstArray with a string value within the $secondArray.
356 * The core method does not support such a overrule.
357 * The reason for this code duplication is that the extbase method will be
358 * deprecated in the future.
359 *
360 * @param array $firstArray First array
361 * @param array $secondArray Second array, overruling the first array
362 * @param bool $dontAddNewKeys If set, keys that are NOT found in $firstArray (first array)
363 * will not be set. Thus only existing value can/will be
364 * overruled from second array.
365 * @param bool $emptyValuesOverride If set (which is the default), values from $secondArray
366 * will overrule if they are empty (according to PHP's empty() function)
367 * @return array Resulting array where $secondArray values has overruled $firstArray values
368 * @internal
369 */
370 protected function mergeRecursiveWithOverrule(
371 array $firstArray,
372 array $secondArray,
373 bool $dontAddNewKeys = false,
374 bool $emptyValuesOverride = true
375 ): array {
376 foreach ($secondArray as $key => $value) {
377 if (
378 array_key_exists($key, $firstArray)
379 && is_array($firstArray[$key])
380 ) {
381 if (is_array($secondArray[$key])) {
382 $firstArray[$key] = $this->mergeRecursiveWithOverrule(
383 $firstArray[$key],
384 $secondArray[$key],
385 $dontAddNewKeys,
386 $emptyValuesOverride
387 );
388 } else {
389 $firstArray[$key] = $secondArray[$key];
390 }
391 } else {
392 if ($dontAddNewKeys) {
393 if (array_key_exists($key, $firstArray)) {
394 if ($emptyValuesOverride || !empty($value)) {
395 $firstArray[$key] = $value;
396 }
397 }
398 } else {
399 if ($emptyValuesOverride || !empty($value)) {
400 $firstArray[$key] = $value;
401 }
402 }
403 }
404 }
405 reset($firstArray);
406 return $firstArray;
407 }
408 }