Passed
Branch master (6c65a4)
by Christian
16:31
created

EvaluateDisplayConditions::evaluateConditions()   F

Complexity

Conditions 31
Paths 2695

Size

Total Lines 114
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 31
eloc 73
nc 2695
nop 1
dl 0
loc 114
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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(
0 ignored issues
show
Bug introduced by
array_keys($flexContext[...ntContainerIdentifier]) cannot be passed to array_shift() as the parameter $array expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

555
                    $containerType = array_shift(/** @scrutinizer ignore-type */ array_keys(
Loading history...
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
}
936