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