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