bcf59409527cefb2847f447c6be8a93d75bbb1c7
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / FormDataProvider / EvaluateDisplayConditions.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Backend\Form\FormDataProvider;
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\Backend\Form\FormDataProviderInterface;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20 use TYPO3\CMS\Core\Utility\MathUtility;
21
22 /**
23 * Class EvaluateDisplayConditions implements the TCA 'displayCond' option.
24 * The display condition is a colon separated string which describes
25 * the condition to decide whether a form field should be displayed.
26 */
27 class EvaluateDisplayConditions implements FormDataProviderInterface
28 {
29 /**
30 * Remove fields from processedTca columns that should not be displayed.
31 *
32 * Strategy of the parser is to first find all displayCond in given tca
33 * and within all type=flex fields to parse them into an array. This condition
34 * array contains all information to evaluate that condition in a second
35 * step that - depending on evaluation result - then throws away or keeps the field.
36 *
37 * @param array $result
38 * @return array
39 */
40 public function addData(array $result): array
41 {
42 $result = $this->parseDisplayConditions($result);
43 $result = $this->evaluateConditions($result);
44 return $result;
45 }
46
47 /**
48 * Find all 'displayCond' in TCA and flex forms and substitute them with an
49 * array representation that contains all relevant data to
50 * evaluate the condition later. For "FIELD" conditions the helper methods
51 * findFieldValue() is used to find the value of the referenced field to put
52 * that value into the returned array, too. This is important since the referenced
53 * field is "relative" to the position of the field that has the display condition.
54 * For instance, "FIELD:aField:=:foo" within a flex form field references a field
55 * value from the same sheet, and there are many more complex scenarios to resolve.
56 *
57 * @param array $result Incoming result array
58 * @throws \RuntimeException
59 * @return array Modified result array with all displayCond parsed into arrays
60 */
61 protected function parseDisplayConditions(array $result): array
62 {
63 $flexColumns = [];
64 foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
65 if ($columnConfiguration['config']['type'] === 'flex') {
66 $flexColumns[$columnName] = $columnConfiguration;
67 }
68 if (!isset($columnConfiguration['displayCond'])) {
69 continue;
70 }
71 $result['processedTca']['columns'][$columnName]['displayCond'] = $this->parseConditionRecursive(
72 $columnConfiguration['displayCond'],
73 $result['databaseRow']
74 );
75 }
76
77 foreach ($flexColumns as $columnName => $flexColumn) {
78 $sheetNameFieldNames = [];
79 foreach ($flexColumn['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
80 // Create a list of all sheet names with field names combinations for later 'sheetName.fieldName' lookups
81 // 'one.sheet.one.field' as key, with array of "sheetName" and "fieldName" as value
82 if (isset($sheetConfiguration['ROOT']['el']) && is_array($sheetConfiguration['ROOT']['el'])) {
83 foreach ($sheetConfiguration['ROOT']['el'] as $flexElementName => $flexElementConfiguration) {
84 // section container have no value in its own
85 if (isset($flexElementConfiguration['type']) && $flexElementConfiguration['type'] === 'array'
86 && isset($flexElementConfiguration['section']) && $flexElementConfiguration['section'] == 1
87 ) {
88 continue;
89 }
90 $combinedKey = $sheetName . '.' . $flexElementName;
91 if (array_key_exists($combinedKey, $sheetNameFieldNames)) {
92 throw new \RuntimeException(
93 'Ambiguous sheet name and field name combination: Sheet "' . $sheetNameFieldNames[$combinedKey]['sheetName']
94 . '" with field name "' . $sheetNameFieldNames[$combinedKey]['fieldName'] . '" overlaps with sheet "'
95 . $sheetName . '" and field name "' . $flexElementName . '". Do not do that.',
96 1481483061
97 );
98 }
99 $sheetNameFieldNames[$combinedKey] = [
100 'sheetName' => $sheetName,
101 'fieldName' => $flexElementName,
102 ];
103 }
104 }
105 }
106 foreach ($flexColumn['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
107 if (isset($sheetConfiguration['ROOT']['displayCond'])) {
108 // Condition on a flex sheet
109 $flexContext = [
110 'context' => 'flexSheet',
111 'sheetNameFieldNames' => $sheetNameFieldNames,
112 'currentSheetName' => $sheetName,
113 'flexFormRowData' => $result['databaseRow'][$columnName],
114 ];
115 $parsedDisplayCondition = $this->parseConditionRecursive(
116 $sheetConfiguration['ROOT']['displayCond'],
117 $result['databaseRow'],
118 $flexContext
119 );
120 $result['processedTca']['columns'][$columnName]['config']['ds']
121 ['sheets'][$sheetName]['ROOT']['displayCond']
122 = $parsedDisplayCondition;
123 }
124 if (isset($sheetConfiguration['ROOT']['el']) && is_array($sheetConfiguration['ROOT']['el'])) {
125 foreach ($sheetConfiguration['ROOT']['el'] as $flexElementName => $flexElementConfiguration) {
126 if (isset($flexElementConfiguration['displayCond'])) {
127 // Condition on a flex element
128 $flexContext = [
129 'context' => 'flexField',
130 'sheetNameFieldNames' => $sheetNameFieldNames,
131 'currentSheetName' => $sheetName,
132 'currentFieldName' => $flexElementName,
133 'flexFormDataStructure' => $result['processedTca']['columns'][$columnName]['config']['ds'],
134 'flexFormRowData' => $result['databaseRow'][$columnName],
135 ];
136 $parsedDisplayCondition = $this->parseConditionRecursive(
137 $flexElementConfiguration['displayCond'],
138 $result['databaseRow'],
139 $flexContext
140 );
141 $result['processedTca']['columns'][$columnName]['config']['ds']
142 ['sheets'][$sheetName]['ROOT']
143 ['el'][$flexElementName]['displayCond']
144 = $parsedDisplayCondition;
145 }
146 if (isset($flexElementConfiguration['type']) && $flexElementConfiguration['type'] === 'array'
147 && isset($flexElementConfiguration['section']) && $flexElementConfiguration['section'] == 1
148 && isset($flexElementConfiguration['children']) && is_array($flexElementConfiguration['children'])
149 ) {
150 // Conditions on flex container section elements
151 foreach ($flexElementConfiguration['children'] as $containerIdentifier => $containerElements) {
152 if (isset($containerElements['el']) && is_array($containerElements['el'])) {
153 foreach ($containerElements['el'] as $containerElementName => $containerElementConfiguration) {
154 if (isset($containerElementConfiguration['displayCond'])) {
155 $flexContext = [
156 'context' => 'flexContainerElement',
157 'sheetNameFieldNames' => $sheetNameFieldNames,
158 'currentSheetName' => $sheetName,
159 'currentFieldName' => $flexElementName,
160 'currentContainerIdentifier' => $containerIdentifier,
161 'currentContainerElementName' => $containerElementName,
162 'flexFormDataStructure' => $result['processedTca']['columns'][$columnName]['config']['ds'],
163 'flexFormRowData' => $result['databaseRow'][$columnName],
164 ];
165 $parsedDisplayCondition = $this->parseConditionRecursive(
166 $containerElementConfiguration['displayCond'],
167 $result['databaseRow'],
168 $flexContext
169 );
170 $result['processedTca']['columns'][$columnName]['config']['ds']
171 ['sheets'][$sheetName]['ROOT']
172 ['el'][$flexElementName]
173 ['children'][$containerIdentifier]
174 ['el'][$containerElementName]['displayCond']
175 = $parsedDisplayCondition;
176 }
177 }
178 }
179 }
180 }
181 }
182 }
183 }
184 }
185 return $result;
186 }
187
188 /**
189 * Parse a condition into an array representation and validate syntax. Handles nested conditions combined with AND and OR.
190 * Calls itself recursive for nesting and logically combined conditions.
191 *
192 * @param mixed $condition Either an array with multiple conditions combined with AND or OR, or a single condition string
193 * @param array $databaseRow Incoming full database row
194 * @param array $flexContext Detailed flex context if display condition is within a flex field, needed to determine field value for "FIELD" conditions
195 * @throws \RuntimeException
196 * @return array Array representation of that condition, see unit tests for details on syntax
197 */
198 protected function parseConditionRecursive($condition, array $databaseRow, array $flexContext = []): array
199 {
200 $conditionArray = [];
201 if (is_string($condition)) {
202 $conditionArray = $this->parseSingleConditionString($condition, $databaseRow, $flexContext);
203 } elseif (is_array($condition)) {
204 foreach ($condition as $logicalOperator => $groupedDisplayConditions) {
205 $logicalOperator = strtoupper($logicalOperator);
206 if (($logicalOperator !== 'AND' && $logicalOperator !== 'OR') || !is_array($groupedDisplayConditions)) {
207 throw new \RuntimeException(
208 'Multiple conditions must have boolean operator "OR" or "AND", "' . $logicalOperator . '" given.',
209 1481380393
210 );
211 }
212 if (count($groupedDisplayConditions) < 2) {
213 throw new \RuntimeException(
214 'With multiple conditions combined by "' . $logicalOperator . '", there must be at least two sub conditions',
215 1481464101
216 );
217 }
218 $conditionArray = [
219 'type' => $logicalOperator,
220 'subConditions' => [],
221 ];
222 foreach ($groupedDisplayConditions as $key => $singleDisplayCondition) {
223 $key = strtoupper((string)$key);
224 if (($key === 'AND' || $key === 'OR') && is_array($singleDisplayCondition)) {
225 // Recursion statement: condition is 'AND' or 'OR' and is pointing to an array (should be conditions again)
226 $conditionArray['subConditions'][] = $this->parseConditionRecursive(
227 [$key => $singleDisplayCondition],
228 $databaseRow,
229 $flexContext
230 );
231 } else {
232 $conditionArray['subConditions'][] = $this->parseConditionRecursive(
233 $singleDisplayCondition,
234 $databaseRow,
235 $flexContext
236 );
237 }
238 }
239 }
240 } else {
241 throw new \RuntimeException(
242 'Condition must be either an array with sub conditions or a single condition string, type ' . gettype($condition) . ' given.',
243 1481381058
244 );
245 }
246 return $conditionArray;
247 }
248
249 /**
250 * Parse a single condition string into pieces, validate them and return
251 * an array representation.
252 *
253 * @param string $conditionString Given condition string like "VERSION:IS:true"
254 * @param array $databaseRow Incoming full database row
255 * @param array $flexContext Detailed flex context if display condition is within a flex field, needed to determine field value for "FIELD" conditions
256 * @return array Validated name array, example: [ type="VERSION", isVersion="true" ]
257 * @throws \RuntimeException
258 */
259 protected function parseSingleConditionString(string $conditionString, array $databaseRow, array $flexContext = []): array
260 {
261 $conditionArray = GeneralUtility::trimExplode(':', $conditionString, false, 4);
262 $namedConditionArray = [
263 'type' => $conditionArray[0],
264 ];
265 switch ($namedConditionArray['type']) {
266 case 'FIELD':
267 if (empty($conditionArray[1])) {
268 throw new \RuntimeException(
269 'Field condition "' . $conditionString . '" must have a field name as second part, none given.'
270 . 'Example: "FIELD:myField:=:myValue"',
271 1481385695
272 );
273 }
274 $fieldName = $conditionArray[1];
275 $allowedOperators = [ 'REQ', '>', '<', '>=', '<=', '-', '!-', '=', '!=', 'IN', '!IN', 'BIT', '!BIT' ];
276 if (empty($conditionArray[2]) || !in_array($conditionArray[2], $allowedOperators)) {
277 throw new \RuntimeException(
278 'Field condition "' . $conditionString . '" must have a valid operator as third part, non or invalid one given.'
279 . ' Valid operators are: "' . implode('", "', $allowedOperators) . '".'
280 . ' Example: "FIELD:myField:=:4"',
281 1481386239
282 );
283 }
284 $namedConditionArray['operator'] = $conditionArray[2];
285 if (!isset($conditionArray[3])) {
286 throw new \RuntimeException(
287 'Field condition "' . $conditionString . '" must have an operand as fourth part, none given.'
288 . ' Example: "FIELD:myField:=:4"',
289 1481401543
290 );
291 }
292 $operand = $conditionArray[3];
293 if ($namedConditionArray['operator'] === 'REQ') {
294 $operand = strtolower($operand);
295 if ($operand === 'true') {
296 $namedConditionArray['operand'] = true;
297 } elseif ($operand === 'false') {
298 $namedConditionArray['operand'] = false;
299 } else {
300 throw new \RuntimeException(
301 'Field condition "' . $conditionString . '" must have "true" or "false" as fourth part.'
302 . ' Example: "FIELD:myField:REQ:true',
303 1481401892
304 );
305 }
306 } elseif (in_array($namedConditionArray['operator'], [ '>', '<', '>=', '<=', 'BIT', '!BIT' ])) {
307 if (!MathUtility::canBeInterpretedAsInteger($operand)) {
308 throw new \RuntimeException(
309 'Field condition "' . $conditionString . '" with comparison operator ' . $namedConditionArray['operator']
310 . ' must have a number as fourth part, ' . $operand . ' given. Example: "FIELD:myField:>:42"',
311 1481456806
312 );
313 }
314 $namedConditionArray['operand'] = (int)$operand;
315 } elseif ($namedConditionArray['operator'] === '-' || $namedConditionArray['operator'] === '!-') {
316 list($minimum, $maximum) = GeneralUtility::trimExplode('-', $operand);
317 if (!MathUtility::canBeInterpretedAsInteger($minimum) || !MathUtility::canBeInterpretedAsInteger($maximum)) {
318 throw new \RuntimeException(
319 'Field condition "' . $conditionString . '" with comparison operator ' . $namedConditionArray['operator']
320 . ' must have two numbers as fourth part, separated by dash, ' . $operand . ' given. Example: "FIELD:myField:-:1-3"',
321 1481457277
322 );
323 }
324 $namedConditionArray['operand'] = '';
325 $namedConditionArray['min'] = (int)$minimum;
326 $namedConditionArray['max'] = (int)$maximum;
327 } elseif ($namedConditionArray['operator'] === 'IN' || $namedConditionArray['operator'] === '!IN'
328 || $namedConditionArray['operator'] === '=' || $namedConditionArray['operator'] === '!='
329 ) {
330 $namedConditionArray['operand'] = $operand;
331 }
332 $namedConditionArray['fieldValue'] = $this->findFieldValue($fieldName, $databaseRow, $flexContext);
333 break;
334 case 'HIDE_FOR_NON_ADMINS':
335 break;
336 case 'REC':
337 if (empty($conditionArray[1]) || $conditionArray[1] !== 'NEW') {
338 throw new \RuntimeException(
339 'Record condition "' . $conditionString . '" must contain "NEW" keyword: either "REC:NEW:true" or "REC:NEW:false"',
340 1481384784
341 );
342 }
343 if (empty($conditionArray[2])) {
344 throw new \RuntimeException(
345 'Record condition "' . $conditionString . '" must have an operand "true" or "false", none given. Example: "REC:NEW:true"',
346 1481384947
347 );
348 }
349 $operand = strtolower($conditionArray[2]);
350 if ($operand === 'true') {
351 $namedConditionArray['isNew'] = true;
352 } elseif ($operand === 'false') {
353 $namedConditionArray['isNew'] = false;
354 } else {
355 throw new \RuntimeException(
356 'Record condition "' . $conditionString . '" must have an operand "true" or "false, example "REC:NEW:true", given: ' . $operand,
357 1481385173
358 );
359 }
360 // Programming error: There must be a uid available, other data providers should have taken care of that already
361 if (!array_key_exists('uid', $databaseRow)) {
362 throw new \RuntimeException(
363 'Required [\'databaseRow\'][\'uid\'] not found in data array',
364 1481467208
365 );
366 }
367 // May contain "NEW123..."
368 $namedConditionArray['uid'] = $databaseRow['uid'];
369 break;
370 case 'VERSION':
371 if (empty($conditionArray[1]) || $conditionArray[1] !== 'IS') {
372 throw new \RuntimeException(
373 'Version condition "' . $conditionString . '" must contain "IS" keyword: either "VERSION:IS:false" or "VERSION:IS:true"',
374 1481383660
375 );
376 }
377 if (empty($conditionArray[2])) {
378 throw new \RuntimeException(
379 'Version condition "' . $conditionString . '" must have an operand "true" or "false", none given. Example: "VERSION:IS:true',
380 1481383888
381 );
382 }
383 $operand = strtolower($conditionArray[2]);
384 if ($operand === 'true') {
385 $namedConditionArray['isVersion'] = true;
386 } elseif ($operand === 'false') {
387 $namedConditionArray['isVersion'] = false;
388 } else {
389 throw new \RuntimeException(
390 'Version condition "' . $conditionString . '" must have a "true" or "false" operand, example "VERSION:IS:true", given: ' . $operand,
391 1481384123
392 );
393 }
394 // Programming error: There must be a uid available, other data providers should have taken care of that already
395 if (!array_key_exists('uid', $databaseRow)) {
396 throw new \RuntimeException(
397 'Required [\'databaseRow\'][\'uid\'] not found in data array',
398 1481469854
399 );
400 }
401 $namedConditionArray['uid'] = $databaseRow['uid'];
402 if (array_key_exists('pid', $databaseRow)) {
403 $namedConditionArray['pid'] = $databaseRow['pid'];
404 }
405 if (array_key_exists('_ORIG_pid', $databaseRow)) {
406 $namedConditionArray['_ORIG_pid'] = $databaseRow['_ORIG_pid'];
407 }
408 break;
409 case 'USER':
410 if (empty($conditionArray[1])) {
411 throw new \RuntimeException(
412 'User function condition "' . $conditionString . '" must have a user function defined a second part, none given.'
413 . ' Correct format is USER:\My\User\Func->match:more:arguments,'
414 . ' given: ' . $conditionString,
415 1481382954
416 );
417 }
418 $namedConditionArray['function'] = $conditionArray[1];
419 array_shift($conditionArray);
420 array_shift($conditionArray);
421 $namedConditionArray['parameters'] = $conditionArray;
422 $namedConditionArray['record'] = $databaseRow;
423 break;
424 default:
425 throw new \RuntimeException(
426 'Unknown condition rule type "' . $namedConditionArray['type'] . '" with display condition "' . $conditionString . '"".',
427 1481381950
428 );
429 }
430 return $namedConditionArray;
431 }
432
433 /**
434 * Find field value the condition refers to for "FIELD:" conditions. For "normal" TCA fields this is the value of
435 * a "neighbor" field, but in flex form context it can be prepended with a sheet name. The method sorts out the
436 * details and returns the current field value.
437 *
438 * @param string $givenFieldName The full name used in displayCond. Can have sheet names included in flex context
439 * @param array $databaseRow Incoming database row values
440 * @param array $flexContext Detailed flex context if display condition is within a flex field, needed to determine field value for "FIELD" conditions
441 * @throws \RuntimeException
442 * @return mixed The current field value from database row or a deeper flex form structure field.
443 */
444 protected function findFieldValue(string $givenFieldName, array $databaseRow, array $flexContext = [])
445 {
446 $fieldValue = null;
447
448 // Early return for "normal" tca fields
449 if (empty($flexContext)) {
450 if (array_key_exists($givenFieldName, $databaseRow)) {
451 $fieldValue = $databaseRow[$givenFieldName];
452 }
453 return $fieldValue;
454 }
455 if ($flexContext['context'] === 'flexSheet') {
456 // A display condition on a flex form sheet. Relatively simple: fieldName is either
457 // "parentRec.fieldName" pointing to a databaseRow field name, or "sheetName.fieldName" pointing
458 // to a field value from a neighbor field.
459 if (strpos($givenFieldName, 'parentRec.') === 0) {
460 $fieldName = substr($givenFieldName, 10);
461 if (array_key_exists($fieldName, $databaseRow)) {
462 $fieldValue = $databaseRow[$fieldName];
463 }
464 } else {
465 if (array_key_exists($givenFieldName, $flexContext['sheetNameFieldNames'])) {
466 if ($flexContext['currentSheetName'] === $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName']) {
467 throw new \RuntimeException(
468 'Configuring displayCond to "' . $givenFieldName . '" on flex form sheet "'
469 . $flexContext['currentSheetName'] . '" referencing a value from the same sheet does not make sense.',
470 1481485705
471 );
472 }
473 }
474 $sheetName = $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName'];
475 $fieldName = $flexContext['sheetNameFieldNames'][$givenFieldName]['fieldName'];
476 if (!isset($flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'])) {
477 throw new \RuntimeException(
478 'Flex form displayCond on sheet "' . $flexContext['currentSheetName'] . '" references field "' . $fieldName
479 . '" of sheet "' . $sheetName . '", but that field does not exist in current data structure',
480 1481488492
481 );
482 }
483 $fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'];
484 }
485 } elseif ($flexContext['context'] === 'flexField') {
486 // A display condition on a flex field. Handle "parentRec." similar to sheet conditions,
487 // get a list of "local" field names and see if they are used as reference, else see if a
488 // "sheetName.fieldName" field reference is given
489 if (strpos($givenFieldName, 'parentRec.') === 0) {
490 $fieldName = substr($givenFieldName, 10);
491 if (array_key_exists($fieldName, $databaseRow)) {
492 $fieldValue = $databaseRow[$fieldName];
493 }
494 } else {
495 $listOfLocalFlexFieldNames = array_keys(
496 $flexContext['flexFormDataStructure']['sheets'][$flexContext['currentSheetName']]['ROOT']['el']
497 );
498 if (in_array($givenFieldName, $listOfLocalFlexFieldNames, true)) {
499 // Condition references field name of the same sheet
500 $sheetName = $flexContext['currentSheetName'];
501 if (!isset($flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$givenFieldName]['vDEF'])) {
502 throw new \RuntimeException(
503 'Flex form displayCond on field "' . $flexContext['currentFieldName'] . '" on flex form sheet "'
504 . $flexContext['currentSheetName'] . '" references field "' . $givenFieldName . '", but a field value'
505 . ' does not exist in this sheet',
506 1481492953
507 );
508 }
509 $fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$givenFieldName]['vDEF'];
510 } elseif (in_array($givenFieldName, array_keys($flexContext['sheetNameFieldNames'], true))) {
511 // Condition references field name including a sheet name
512 $sheetName = $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName'];
513 $fieldName = $flexContext['sheetNameFieldNames'][$givenFieldName]['fieldName'];
514 $fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'];
515 } else {
516 throw new \RuntimeException(
517 'Flex form displayCond on field "' . $flexContext['currentFieldName'] . '" on flex form sheet "'
518 . $flexContext['currentSheetName'] . '" references a field or field / sheet combination "'
519 . $givenFieldName . '" that might be defined in given data structure but is not found in data values.',
520 1481496170
521 );
522 }
523 }
524 } elseif ($flexContext['context'] === 'flexContainerElement') {
525 // A display condition on a flex form section container element. Handle "parentRec.", compare to a
526 // list of local field names, compare to a list of field names from same sheet, compare to a list
527 // of sheet fields from other sheets.
528 if (strpos($givenFieldName, 'parentRec.') === 0) {
529 $fieldName = substr($givenFieldName, 10);
530 if (array_key_exists($fieldName, $databaseRow)) {
531 $fieldValue = $databaseRow[$fieldName];
532 }
533 } else {
534 $currentSheetName = $flexContext['currentSheetName'];
535 $currentFieldName = $flexContext['currentFieldName'];
536 $currentContainerIdentifier = $flexContext['currentContainerIdentifier'];
537 $currentContainerElementName = $flexContext['currentContainerElementName'];
538 $listOfLocalContainerElementNames = array_keys(
539 $flexContext['flexFormDataStructure']['sheets'][$currentSheetName]['ROOT']
540 ['el'][$currentFieldName]
541 ['children'][$currentContainerIdentifier]
542 ['el']
543 );
544 $listOfLocalContainerElementNamesWithSheetName = [];
545 foreach ($listOfLocalContainerElementNames as $aContainerElementName) {
546 $listOfLocalContainerElementNamesWithSheetName[$currentSheetName . '.' . $aContainerElementName] = [
547 'containerElementName' => $aContainerElementName,
548 ];
549 }
550 $listOfLocalFlexFieldNames = array_keys(
551 $flexContext['flexFormDataStructure']['sheets'][$currentSheetName]['ROOT']['el']
552 );
553 if (in_array($givenFieldName, $listOfLocalContainerElementNames, true)) {
554 // Condition references field of same container instance
555 $containerType = array_shift(array_keys(
556 $flexContext['flexFormRowData']['data'][$currentSheetName]
557 ['lDEF'][$currentFieldName]
558 ['el'][$currentContainerIdentifier]
559 ));
560 $fieldValue = $flexContext['flexFormRowData']['data'][$currentSheetName]
561 ['lDEF'][$currentFieldName]
562 ['el'][$currentContainerIdentifier]
563 [$containerType]
564 ['el'][$givenFieldName]['vDEF'];
565 } elseif (in_array($givenFieldName, array_keys($listOfLocalContainerElementNamesWithSheetName, true))) {
566 // Condition references field name of same container instance and has sheet name included
567 $containerType = array_shift(array_keys(
568 $flexContext['flexFormRowData']['data'][$currentSheetName]
569 ['lDEF'][$currentFieldName]
570 ['el'][$currentContainerIdentifier]
571 ));
572 $fieldName = $listOfLocalContainerElementNamesWithSheetName[$givenFieldName]['containerElementName'];
573 $fieldValue = $flexContext['flexFormRowData']['data'][$currentSheetName]
574 ['lDEF'][$currentFieldName]
575 ['el'][$currentContainerIdentifier]
576 [$containerType]
577 ['el'][$fieldName]['vDEF'];
578 } elseif (in_array($givenFieldName, $listOfLocalFlexFieldNames, true)) {
579 // Condition reference field name of sheet this section container is in
580 $fieldValue = $flexContext['flexFormRowData']['data'][$currentSheetName]
581 ['lDEF'][$givenFieldName]['vDEF'];
582 } elseif (in_array($givenFieldName, array_keys($flexContext['sheetNameFieldNames'], true))) {
583 $sheetName = $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName'];
584 $fieldName = $flexContext['sheetNameFieldNames'][$givenFieldName]['fieldName'];
585 $fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'];
586 } else {
587 $containerType = array_shift(array_keys(
588 $flexContext['flexFormRowData']['data'][$currentSheetName]
589 ['lDEF'][$currentFieldName]
590 ['el'][$currentContainerIdentifier]
591 ));
592 throw new \RuntimeException(
593 'Flex form displayCond on section container field "' . $currentContainerElementName . '" of container type "'
594 . $containerType . '" on flex form sheet "'
595 . $flexContext['currentSheetName'] . '" references a field or field / sheet combination "'
596 . $givenFieldName . '" that might be defined in given data structure but is not found in data values.',
597 1481634649
598 );
599 }
600 }
601 }
602
603 return $fieldValue;
604 }
605
606 /**
607 * Loop through TCA, find prepared conditions and evaluate them. Delete either the
608 * field itself if the condition did not match, or the 'displayCond' in TCA.
609 *
610 * @param array $result
611 * @return array
612 */
613 protected function evaluateConditions(array $result): array
614 {
615 // Evaluate normal tca fields first
616 $listOfFlexFieldNames = [];
617 foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
618 $conditionResult = true;
619 if (isset($columnConfiguration['displayCond'])) {
620 $conditionResult = $this->evaluateConditionRecursive($columnConfiguration['displayCond']);
621 if (!$conditionResult) {
622 unset($result['processedTca']['columns'][$columnName]);
623 } else {
624 // Always unset the whole parsed display condition to save some memory, we're done with them
625 unset($result['processedTca']['columns'][$columnName]['displayCond']);
626 }
627 }
628 // If field was not removed and if it is a flex field, add to list of flex fields to scan
629 if ($conditionResult && $columnConfiguration['config']['type'] === 'flex') {
630 $listOfFlexFieldNames[] = $columnName;
631 }
632 }
633
634 // Search for flex fields and evaluate sheet conditions throwing them away if needed
635 foreach ($listOfFlexFieldNames as $columnName) {
636 $columnConfiguration = $result['processedTca']['columns'][$columnName];
637 foreach ($columnConfiguration['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
638 if (is_array($sheetConfiguration['ROOT']['displayCond'])) {
639 if (!$this->evaluateConditionRecursive($sheetConfiguration['ROOT']['displayCond'])) {
640 unset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName]);
641 } else {
642 unset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName]['ROOT']['displayCond']);
643 }
644 }
645 }
646 }
647
648 // With full sheets gone we loop over display conditions of single fields in flex to throw fields away if needed
649 $listOfFlexSectionContainers = [];
650 foreach ($listOfFlexFieldNames as $columnName) {
651 $columnConfiguration = $result['processedTca']['columns'][$columnName];
652 if (is_array($columnConfiguration['config']['ds']['sheets'])) {
653 foreach ($columnConfiguration['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
654 if (is_array($sheetConfiguration['ROOT']['el'])) {
655 foreach ($sheetConfiguration['ROOT']['el'] as $flexField => $flexConfiguration) {
656 $conditionResult = true;
657 if (is_array($flexConfiguration['displayCond'])) {
658 $conditionResult = $this->evaluateConditionRecursive($flexConfiguration['displayCond']);
659 if (!$conditionResult) {
660 unset(
661 $result['processedTca']['columns'][$columnName]['config']['ds']
662 ['sheets'][$sheetName]['ROOT']
663 ['el'][$flexField]
664 );
665 } else {
666 unset(
667 $result['processedTca']['columns'][$columnName]['config']['ds']
668 ['sheets'][$sheetName]['ROOT']
669 ['el'][$flexField]['displayCond']
670 );
671 }
672 }
673 // If it was not removed and if the field is a section container, add it to the section container list
674 if ($conditionResult
675 && isset($flexConfiguration['type']) && $flexConfiguration['type'] === 'array'
676 && isset($flexConfiguration['section']) && $flexConfiguration['section'] == 1
677 && isset($flexConfiguration['children']) && is_array($flexConfiguration['children'])
678 ) {
679 $listOfFlexSectionContainers[] = [
680 'columnName' => $columnName,
681 'sheetName' => $sheetName,
682 'flexField' => $flexField,
683 ];
684 }
685 }
686 }
687 }
688 }
689 }
690
691 // Loop over found section container elements and evaluate their conditions
692 foreach ($listOfFlexSectionContainers as $flexSectionContainerPosition) {
693 $columnName = $flexSectionContainerPosition['columnName'];
694 $sheetName = $flexSectionContainerPosition['sheetName'];
695 $flexField = $flexSectionContainerPosition['flexField'];
696 $sectionElement = $result['processedTca']['columns'][$columnName]['config']['ds']
697 ['sheets'][$sheetName]['ROOT']
698 ['el'][$flexField];
699 foreach ($sectionElement['children'] as $containerInstanceName => $containerDataStructure) {
700 if (isset($containerDataStructure['el']) && is_array($containerDataStructure['el'])) {
701 foreach ($containerDataStructure['el'] as $containerElementName => $containerElementConfiguration) {
702 if (is_array($containerElementConfiguration['displayCond'])) {
703 if (!$this->evaluateConditionRecursive($containerElementConfiguration['displayCond'])) {
704 unset(
705 $result['processedTca']['columns'][$columnName]['config']['ds']
706 ['sheets'][$sheetName]['ROOT']
707 ['el'][$flexField]
708 ['children'][$containerInstanceName]
709 ['el'][$containerElementName]
710 );
711 } else {
712 unset(
713 $result['processedTca']['columns'][$columnName]['config']['ds']
714 ['sheets'][$sheetName]['ROOT']
715 ['el'][$flexField]
716 ['children'][$containerInstanceName]
717 ['el'][$containerElementName]['displayCond']
718 );
719 }
720 }
721 }
722 }
723 }
724 }
725
726 return $result;
727 }
728
729 /**
730 * Evaluate a condition recursive by evaluating the single condition type
731 *
732 * @param array $conditionArray The condition to evaluate, possibly with subConditions for AND and OR types
733 * @return bool true if the condition matched
734 */
735 protected function evaluateConditionRecursive(array $conditionArray): bool
736 {
737 switch ($conditionArray['type']) {
738 case 'AND':
739 $result = true;
740 foreach ($conditionArray['subConditions'] as $subCondition) {
741 $result = $result && $this->evaluateConditionRecursive($subCondition);
742 }
743 return $result;
744 case 'OR':
745 $result = false;
746 foreach ($conditionArray['subConditions'] as $subCondition) {
747 $result = $result || $this->evaluateConditionRecursive($subCondition);
748 }
749 return $result;
750 case 'FIELD':
751 return $this->matchFieldCondition($conditionArray);
752 case 'HIDE_FOR_NON_ADMINS':
753 return (bool)$this->getBackendUser()->isAdmin();
754 case 'REC':
755 return $this->matchRecordCondition($conditionArray);
756 case 'VERSION':
757 return $this->matchVersionCondition($conditionArray);
758 case 'USER':
759 return $this->matchUserCondition($conditionArray);
760 }
761 return false;
762 }
763
764 /**
765 * Evaluates conditions concerning a field of the current record.
766 *
767 * Example:
768 * "FIELD:sys_language_uid:>:0" => TRUE, if the field 'sys_language_uid' is greater than 0
769 *
770 * @param array $condition Condition array
771 * @return bool
772 */
773 protected function matchFieldCondition(array $condition): bool
774 {
775 $operator = $condition['operator'];
776 $operand = $condition['operand'];
777 $fieldValue = $condition['fieldValue'];
778 $result = false;
779 switch ($operator) {
780 case 'REQ':
781 if (is_array($fieldValue) && count($fieldValue) <= 1) {
782 $fieldValue = array_shift($fieldValue);
783 }
784 if ($operand) {
785 $result = (bool)$fieldValue;
786 } else {
787 $result = !$fieldValue;
788 }
789 break;
790 case '>':
791 if (is_array($fieldValue) && count($fieldValue) <= 1) {
792 $fieldValue = array_shift($fieldValue);
793 }
794 $result = $fieldValue > $operand;
795 break;
796 case '<':
797 if (is_array($fieldValue) && count($fieldValue) <= 1) {
798 $fieldValue = array_shift($fieldValue);
799 }
800 $result = $fieldValue < $operand;
801 break;
802 case '>=':
803 if (is_array($fieldValue) && count($fieldValue) <= 1) {
804 $fieldValue = array_shift($fieldValue);
805 }
806 if ($fieldValue === null) {
807 // If field value is null, this is NOT greater than or equal 0
808 // See test set "Field is not greater than or equal to zero if empty array given"
809 $result = false;
810 } else {
811 $result = $fieldValue >= $operand;
812 }
813 break;
814 case '<=':
815 if (is_array($fieldValue) && count($fieldValue) <= 1) {
816 $fieldValue = array_shift($fieldValue);
817 }
818 $result = $fieldValue <= $operand;
819 break;
820 case '-':
821 case '!-':
822 if (is_array($fieldValue) && count($fieldValue) <= 1) {
823 $fieldValue = array_shift($fieldValue);
824 }
825 $min = $condition['min'];
826 $max = $condition['max'];
827 $result = $fieldValue >= $min && $fieldValue <= $max;
828 if ($operator[0] === '!') {
829 $result = !$result;
830 }
831 break;
832 case '=':
833 case '!=':
834 if (is_array($fieldValue) && count($fieldValue) <= 1) {
835 $fieldValue = array_shift($fieldValue);
836 }
837 $result = $fieldValue == $operand;
838 if ($operator[0] === '!') {
839 $result = !$result;
840 }
841 break;
842 case 'IN':
843 case '!IN':
844 if (is_array($fieldValue)) {
845 $result = count(array_intersect($fieldValue, GeneralUtility::trimExplode(',', $operand))) > 0;
846 } else {
847 $result = GeneralUtility::inList($operand, $fieldValue);
848 }
849 if ($operator[0] === '!') {
850 $result = !$result;
851 }
852 break;
853 case 'BIT':
854 case '!BIT':
855 $result = (bool)((int)$fieldValue & $operand);
856 if ($operator[0] === '!') {
857 $result = !$result;
858 }
859 break;
860 }
861 return $result;
862 }
863
864 /**
865 * Evaluates conditions concerning the status of the current record.
866 *
867 * Example:
868 * "REC:NEW:FALSE" => TRUE, if the record is already persisted (has a uid > 0)
869 *
870 * @param array $condition Condition array
871 * @return bool
872 */
873 protected function matchRecordCondition(array $condition): bool
874 {
875 if ($condition['isNew']) {
876 return !((int)$condition['uid'] > 0);
877 }
878 return (int)$condition['uid'] > 0;
879 }
880
881 /**
882 * Evaluates whether the current record is versioned.
883 *
884 * @param array $condition Condition array
885 * @return bool
886 */
887 protected function matchVersionCondition(array $condition): bool
888 {
889 $isNewRecord = !((int)$condition['uid'] > 0);
890 // Detection of version can be done by detecting the workspace of the user
891 $isUserInWorkspace = $this->getBackendUser()->workspace > 0;
892 if ((array_key_exists('pid', $condition) && (int)$condition['pid'] === -1)
893 || (array_key_exists('_ORIG_pid', $condition) && (int)$condition['_ORIG_pid'] === -1)
894 ) {
895 $isRecordDetectedAsVersion = true;
896 } else {
897 $isRecordDetectedAsVersion = false;
898 }
899 // New records in a workspace are not handled as a version record
900 // if it's no new version, we detect versions like this:
901 // * if user is in workspace: always TRUE
902 // * if editor is in live ws: only TRUE if pid == -1
903 $result = ($isUserInWorkspace || $isRecordDetectedAsVersion) && !$isNewRecord;
904 if (!$condition['isVersion']) {
905 $result = !$result;
906 }
907 return $result;
908 }
909
910 /**
911 * Evaluates via the referenced user-defined method
912 *
913 * @param array $condition Condition array
914 * @return bool
915 */
916 protected function matchUserCondition(array $condition): bool
917 {
918 $parameter = [
919 'record' => $condition['record'],
920 'flexformValueKey' => 'vDEF',
921 'conditionParameters' => $condition['parameters'],
922 ];
923 return (bool)GeneralUtility::callUserFunction($condition['function'], $parameter, $this);
924 }
925
926 /**
927 * Get current backend user
928 *
929 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
930 */
931 protected function getBackendUser()
932 {
933 return $GLOBALS['BE_USER'];
934 }
935 }