8c30e23b98a2ff86057ac9b37e0bc8cecaa13d67
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / FormDataProvider / EvaluateDisplayConditions.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\FormDataProvider;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19
20 /**
21 * Class EvaluateDisplayConditions implements the TCA 'displayCond' option.
22 * The display condition is a colon separated string which describes
23 * the condition to decide whether a form field should be displayed.
24 */
25 class EvaluateDisplayConditions implements FormDataProviderInterface
26 {
27 /**
28 * Remove fields from processedTca columns that should not be displayed.
29 *
30 * @param array $result
31 * @return array
32 */
33 public function addData(array $result)
34 {
35 $result = $this->removeFlexformFields($result);
36 $result = $this->removeFlexformSheets($result);
37 $result = $this->removeTcaColumns($result);
38
39 return $result;
40 }
41
42 /**
43 * Evaluate the TCA column display conditions and remove columns that are not displayed
44 *
45 * @param array $result
46 * @return array
47 */
48 protected function removeTcaColumns($result)
49 {
50 foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
51 if (!isset($columnConfiguration['displayCond'])) {
52 continue;
53 }
54
55 if (!$this->evaluateDisplayCondition($columnConfiguration['displayCond'], $result['databaseRow'])) {
56 unset($result['processedTca']['columns'][$columnName]);
57 }
58 }
59
60 return $result;
61 }
62
63 /**
64 * Remove flexform sheets from processed tca if hidden by display conditions
65 *
66 * @param array $result
67 * @return array
68 */
69 protected function removeFlexformSheets($result)
70 {
71 foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
72 if (!isset($columnConfiguration['config']['type'])
73 || $columnConfiguration['config']['type'] !== 'flex'
74 || !isset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'])
75 || !is_array($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'])
76 ) {
77 continue;
78 }
79
80 $flexFormRowData = is_array($result['databaseRow'][$columnName]['data']) ? $result['databaseRow'][$columnName]['data'] : [];
81 $flexFormRowData = $this->flattenFlexformRowData($flexFormRowData);
82 $flexFormRowData['parentRec'] = $result['databaseRow'];
83
84 $flexFormSheets = $result['processedTca']['columns'][$columnName]['config']['ds']['sheets'];
85 foreach ($flexFormSheets as $sheetName => $sheetConfiguration) {
86 if (!isset($sheetConfiguration['ROOT']['displayCond'])) {
87 continue;
88 }
89 if (!$this->evaluateDisplayCondition($sheetConfiguration['ROOT']['displayCond'], $flexFormRowData, true)) {
90 unset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName]);
91 }
92 }
93 }
94
95 return $result;
96 }
97
98 /**
99 * Remove fields from flexform sheets if hidden by display conditions
100 *
101 * @param array $result
102 * @return array
103 */
104 protected function removeFlexformFields($result)
105 {
106 foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
107 if (!isset($columnConfiguration['config']['type'])
108 || $columnConfiguration['config']['type'] !== 'flex'
109 || !isset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'])
110 || !is_array($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'])
111 ) {
112 continue;
113 }
114
115 $flexFormRowData = is_array($result['databaseRow'][$columnName]['data']) ? $result['databaseRow'][$columnName]['data'] : [];
116 $flexFormRowData['parentRec'] = $result['databaseRow'];
117
118 foreach ($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
119 $flexFormSheetRowData = $flexFormRowData[$sheetName]['lDEF'];
120 $flexFormSheetRowData['parentRec'] = $result['databaseRow'];
121 $result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName] = $this->removeFlexformFieldsRecursive(
122 $result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName],
123 $flexFormSheetRowData
124 );
125 }
126 }
127
128 return $result;
129 }
130
131 /**
132 * Remove fields from flexform data structure
133 *
134 * @param array $structure Given hierarchy
135 * @param array $flexFormRowData
136 * @return array Modified hierarchy
137 */
138 protected function removeFlexformFieldsRecursive($structure, $flexFormRowData)
139 {
140 $newStructure = [];
141 foreach ($structure as $key => $value) {
142 if ($key === 'el' && is_array($value)) {
143 $newSubStructure = [];
144 foreach ($value as $subKey => $subValue) {
145 if (!isset($subValue['displayCond']) || $this->evaluateDisplayCondition($subValue['displayCond'], $flexFormRowData, true)) {
146 $newSubStructure[$subKey] = $subValue;
147 }
148 }
149 $value = $newSubStructure;
150 }
151 if (is_array($value)) {
152 $value = $this->removeFlexformFieldsRecursive($value, $flexFormRowData);
153 }
154 $newStructure[$key] = $value;
155 }
156
157 return $newStructure;
158 }
159
160 /**
161 * Flatten the Flexform data row for sheet level display conditions that use SheetName.FieldName
162 *
163 * @param array $flexFormRowData
164 * @return array
165 */
166 protected function flattenFlexformRowData($flexFormRowData)
167 {
168 $flatFlexFormRowData = [];
169 foreach ($flexFormRowData as $sheetName => $sheetConfiguration) {
170 foreach ($sheetConfiguration['lDEF'] as $fieldName => $fieldConfiguration) {
171 $flatFlexFormRowData[$sheetName . '.' . $fieldName] = $fieldConfiguration;
172 }
173 }
174
175 return $flatFlexFormRowData;
176 }
177
178 /**
179 * Evaluates the provided condition and returns TRUE if the form
180 * element should be displayed.
181 *
182 * The condition string is separated by colons and the first part
183 * indicates what type of evaluation should be performed.
184 *
185 * @param string $displayCondition
186 * @param array $record
187 * @param bool $flexformContext
188 * @param int $recursionLevel Internal level of recursion
189 * @return bool TRUE if condition evaluates successfully
190 */
191 protected function evaluateDisplayCondition($displayCondition, array $record = [], $flexformContext = false, $recursionLevel = 0)
192 {
193 if ($recursionLevel > 99) {
194 // This should not happen, treat as misconfiguration
195 return true;
196 }
197 if (!is_array($displayCondition)) {
198 // DisplayCondition is not an array - just get its value
199 $result = $this->evaluateSingleDisplayCondition($displayCondition, $record, $flexformContext);
200 } else {
201 // Multiple conditions given as array ('AND|OR' => condition array)
202 $conditionEvaluations = [
203 'AND' => [],
204 'OR' => [],
205 ];
206 foreach ($displayCondition as $logicalOperator => $groupedDisplayConditions) {
207 $logicalOperator = strtoupper($logicalOperator);
208 if (($logicalOperator !== 'AND' && $logicalOperator !== 'OR') || !is_array($groupedDisplayConditions)) {
209 // Invalid line. Skip it.
210 continue;
211 } else {
212 foreach ($groupedDisplayConditions as $key => $singleDisplayCondition) {
213 $key = strtoupper($key);
214 if (($key === 'AND' || $key === 'OR') && is_array($singleDisplayCondition)) {
215 // Recursion statement: condition is 'AND' or 'OR' and is pointing to an array (should be conditions again)
216 $conditionEvaluations[$logicalOperator][] = $this->evaluateDisplayCondition(
217 [$key => $singleDisplayCondition],
218 $record,
219 $flexformContext,
220 $recursionLevel + 1
221 );
222 } else {
223 // Condition statement: collect evaluation of this single condition.
224 $conditionEvaluations[$logicalOperator][] = $this->evaluateSingleDisplayCondition(
225 $singleDisplayCondition,
226 $record,
227 $flexformContext
228 );
229 }
230 }
231 }
232 }
233 if (!empty($conditionEvaluations['OR']) && in_array(true, $conditionEvaluations['OR'], true)) {
234 // There are OR conditions and at least one of them is TRUE
235 $result = true;
236 } elseif (!empty($conditionEvaluations['AND']) && !in_array(false, $conditionEvaluations['AND'], true)) {
237 // There are AND conditions and none of them is FALSE
238 $result = true;
239 } elseif (!empty($conditionEvaluations['OR']) || !empty($conditionEvaluations['AND'])) {
240 // There are some conditions. But no OR was TRUE and at least one AND was FALSE
241 $result = false;
242 } else {
243 // There are no proper conditions - misconfiguration. Return TRUE.
244 $result = true;
245 }
246 }
247 return $result;
248 }
249
250 /**
251 * Evaluates the provided condition and returns TRUE if the form
252 * element should be displayed.
253 *
254 * The condition string is separated by colons and the first part
255 * indicates what type of evaluation should be performed.
256 *
257 * @param string $displayCondition
258 * @param array $record
259 * @param bool $flexformContext
260 * @return bool
261 * @see evaluateDisplayCondition()
262 */
263 protected function evaluateSingleDisplayCondition($displayCondition, array $record = [], $flexformContext = false)
264 {
265 $result = false;
266 list($matchType, $condition) = explode(':', $displayCondition, 2);
267 switch ($matchType) {
268 case 'FIELD':
269 $result = $this->matchFieldCondition($condition, $record, $flexformContext);
270 break;
271 case 'HIDE_FOR_NON_ADMINS':
272 $result = $this->matchHideForNonAdminsCondition();
273 break;
274 case 'REC':
275 $result = $this->matchRecordCondition($condition, $record);
276 break;
277 case 'VERSION':
278 $result = $this->matchVersionCondition($condition, $record);
279 break;
280 case 'USER':
281 $result = $this->matchUserCondition($condition, $record);
282 break;
283 }
284 return $result;
285 }
286
287 /**
288 * Evaluates conditions concerning a field of the current record.
289 * Requires a record set via ->setRecord()
290 *
291 * Example:
292 * "FIELD:sys_language_uid:>:0" => TRUE, if the field 'sys_language_uid' is greater than 0
293 *
294 * @param string $condition
295 * @param array $record
296 * @param bool $flexformContext
297 * @return bool
298 */
299 protected function matchFieldCondition($condition, $record, $flexformContext = false)
300 {
301 list($fieldName, $operator, $operand) = explode(':', $condition, 3);
302 if ($flexformContext) {
303 if (strpos($fieldName, 'parentRec.') !== false) {
304 $fieldNameParts = explode('.', $fieldName, 2);
305 $fieldValue = $record['parentRec'][$fieldNameParts[1]];
306 } else {
307 $fieldValue = $record[$fieldName]['vDEF'];
308 }
309 } else {
310 $fieldValue = $record[$fieldName];
311 }
312 $result = false;
313 switch ($operator) {
314 case 'REQ':
315 if (is_array($fieldValue) && count($fieldValue) <= 1) {
316 $fieldValue = array_shift($fieldValue);
317 }
318 if (strtoupper($operand) === 'TRUE') {
319 $result = (bool)$fieldValue;
320 } else {
321 $result = !$fieldValue;
322 }
323 break;
324 case '>':
325 if (is_array($fieldValue) && count($fieldValue) <= 1) {
326 $fieldValue = array_shift($fieldValue);
327 }
328 $result = $fieldValue > $operand;
329 break;
330 case '<':
331 if (is_array($fieldValue) && count($fieldValue) <= 1) {
332 $fieldValue = array_shift($fieldValue);
333 }
334 $result = $fieldValue < $operand;
335 break;
336 case '>=':
337 if (is_array($fieldValue) && count($fieldValue) <= 1) {
338 $fieldValue = array_shift($fieldValue);
339 }
340 $result = $fieldValue >= $operand;
341 break;
342 case '<=':
343 if (is_array($fieldValue) && count($fieldValue) <= 1) {
344 $fieldValue = array_shift($fieldValue);
345 }
346 $result = $fieldValue <= $operand;
347 break;
348 case '-':
349 case '!-':
350 if (is_array($fieldValue) && count($fieldValue) <= 1) {
351 $fieldValue = array_shift($fieldValue);
352 }
353 list($minimum, $maximum) = explode('-', $operand);
354 $result = $fieldValue >= $minimum && $fieldValue <= $maximum;
355 if ($operator[0] === '!') {
356 $result = !$result;
357 }
358 break;
359 case '=':
360 case '!=':
361 if (is_array($fieldValue) && count($fieldValue) <= 1) {
362 $fieldValue = array_shift($fieldValue);
363 }
364 $result = $fieldValue == $operand;
365 if ($operator[0] === '!') {
366 $result = !$result;
367 }
368 break;
369 case 'IN':
370 case '!IN':
371 if (is_array($fieldValue)) {
372 $result = count(array_intersect($fieldValue, explode(',', $operand))) > 0;
373 } else {
374 $result = GeneralUtility::inList($operand, $fieldValue);
375 }
376 if ($operator[0] === '!') {
377 $result = !$result;
378 }
379 break;
380 case 'BIT':
381 case '!BIT':
382 $result = (bool)((int)$fieldValue & $operand);
383 if ($operator[0] === '!') {
384 $result = !$result;
385 }
386 break;
387 }
388 return $result;
389 }
390
391 /**
392 * Evaluates TRUE if current backend user is an admin.
393 *
394 * @return bool
395 */
396 protected function matchHideForNonAdminsCondition()
397 {
398 return (bool)$this->getBackendUser()->isAdmin();
399 }
400
401 /**
402 * Evaluates conditions concerning the status of the current record.
403 * Requires a record set via ->setRecord()
404 *
405 * Example:
406 * "REC:NEW:FALSE" => TRUE, if the record is already persisted (has a uid > 0)
407 *
408 * @param string $condition
409 * @param array $record
410 * @return bool
411 */
412 protected function matchRecordCondition($condition, $record)
413 {
414 $result = false;
415 list($operator, $operand) = explode(':', $condition, 2);
416 if ($operator === 'NEW') {
417 if (strtoupper($operand) === 'TRUE') {
418 $result = !((int)$record['uid'] > 0);
419 } elseif (strtoupper($operand) === 'FALSE') {
420 $result = ((int)$record['uid'] > 0);
421 }
422 }
423 return $result;
424 }
425
426 /**
427 * Evaluates whether the current record is versioned.
428 * Requires a record set via ->setRecord()
429 *
430 * @param string $condition
431 * @param array $record
432 * @return bool
433 */
434 protected function matchVersionCondition($condition, $record)
435 {
436 $result = false;
437 list($operator, $operand) = explode(':', $condition, 2);
438 if ($operator === 'IS') {
439 $isNewRecord = !((int)$record['uid'] > 0);
440 // Detection of version can be done be detecting the workspace of the user
441 $isUserInWorkspace = $this->getBackendUser()->workspace > 0;
442 if ((int)$record['pid'] === -1 || (int)$record['_ORIG_pid'] === -1) {
443 $isRecordDetectedAsVersion = true;
444 } else {
445 $isRecordDetectedAsVersion = false;
446 }
447 // New records in a workspace are not handled as a version record
448 // if it's no new version, we detect versions like this:
449 // -- if user is in workspace: always TRUE
450 // -- if editor is in live ws: only TRUE if pid == -1
451 $isVersion = ($isUserInWorkspace || $isRecordDetectedAsVersion) && !$isNewRecord;
452 if (strtoupper($operand) === 'TRUE') {
453 $result = $isVersion;
454 } elseif (strtoupper($operand) === 'FALSE') {
455 $result = !$isVersion;
456 }
457 }
458 return $result;
459 }
460
461 /**
462 * Evaluates via the referenced user-defined method
463 *
464 * @param string $condition
465 * @param array $record
466 * @return bool
467 */
468 protected function matchUserCondition($condition, $record)
469 {
470 $conditionParameters = explode(':', $condition);
471 $userFunction = array_shift($conditionParameters);
472
473 $parameter = [
474 'record' => $record,
475 'flexformValueKey' => 'vDEF',
476 'conditionParameters' => $conditionParameters
477 ];
478
479 return (bool)GeneralUtility::callUserFunction($userFunction, $parameter, $this);
480 }
481
482 /**
483 * Get current backend user
484 *
485 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
486 */
487 protected function getBackendUser()
488 {
489 return $GLOBALS['BE_USER'];
490 }
491 }