[FEATURE] Support multiple display conditions in TCA
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / ElementConditionMatcher.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2013 Sebastian Michaelsen (michaelsen@t3seo.de)
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 * A copy is found in the textfile GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
20 *
21 * This script is distributed in the hope that it will be useful,
22 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 * GNU General Public License for more details.
25 *
26 * This copyright notice MUST APPEAR in all copies of the script!
27 ***************************************************************/
28
29 /**
30 * Class ElementConditionMatcher implements the TCA 'displayCond' option.
31 * The display condition is a colon separated string which describes
32 * the condition to decide whether a form field should be displayed.
33 */
34 class ElementConditionMatcher {
35
36 /**
37 * @var string
38 */
39 protected $flexformValueKey = '';
40
41 /**
42 * @var array
43 */
44 protected $record = array();
45
46 /**
47 * Evaluates the provided condition and returns TRUE if the form
48 * element should be displayed.
49 *
50 * The condition string is separated by colons and the first part
51 * indicates what type of evaluation should be performed.
52 *
53 * @param string $displayCondition
54 * @param array $record
55 * @param string $flexformValueKey
56 * @param integer $recursionLevel Internal level of recursion
57 * @return boolean TRUE if condition evaluates successfully
58 */
59 public function match($displayCondition, array $record = array(), $flexformValueKey = '', $recursionLevel = 0) {
60 if ($recursionLevel > 99) {
61 // This should not happen, treat as misconfiguration
62 return TRUE;
63 }
64 if (!is_array($displayCondition)) {
65 // DisplayCondition is not an array - just get its value
66 $result = $this->matchSingle($displayCondition, $record, $flexformValueKey);
67 } else {
68 // Multiple conditions given as array ('AND|OR' => condition array)
69 $conditionEvaluations = array(
70 'AND' => array(),
71 'OR' => array(),
72 );
73 foreach ($displayCondition as $logicalOperator => $groupedDisplayConditions) {
74 $logicalOperator = strtoupper($logicalOperator);
75 if (($logicalOperator !== 'AND' && $logicalOperator !== 'OR') || !is_array($groupedDisplayConditions)) {
76 // Invalid line. Skip it.
77 continue;
78 } else {
79 foreach ($groupedDisplayConditions as $key => $singleDisplayCondition) {
80 $key = strtoupper($key);
81 if (($key === 'AND' || $key === 'OR') && is_array($singleDisplayCondition)) {
82 // Recursion statement: condition is 'AND' or 'OR' and is pointing to an array (should be conditions again)
83 $conditionEvaluations[$logicalOperator][] = $this->match(
84 array($key => $singleDisplayCondition),
85 $record,
86 $flexformValueKey,
87 $recursionLevel + 1
88 );
89 } else {
90 // Condition statement: collect evaluation of this single condition.
91 $conditionEvaluations[$logicalOperator][] = $this->matchSingle(
92 $singleDisplayCondition,
93 $record,
94 $flexformValueKey
95 );
96 }
97 }
98 }
99 }
100 if (count($conditionEvaluations['OR']) > 0 && in_array(TRUE, $conditionEvaluations['OR'], TRUE)) {
101 // There are OR conditions and at least one of them is TRUE
102 $result = TRUE;
103 } elseif (count($conditionEvaluations['AND']) > 0 && !in_array(FALSE, $conditionEvaluations['AND'], TRUE)) {
104 // There are AND conditions and none of them is FALSE
105 $result = TRUE;
106 } elseif (count($conditionEvaluations['OR']) > 0 || count($conditionEvaluations['AND']) > 0) {
107 // There are some conditions. But no OR was TRUE and at least one AND was FALSE
108 $result = FALSE;
109 } else {
110 // There are no proper conditions - misconfiguration. Return TRUE.
111 $result = TRUE;
112 }
113 }
114 return $result;
115 }
116
117 /**
118 * Evaluates the provided condition and returns TRUE if the form
119 * element should be displayed.
120 *
121 * The condition string is separated by colons and the first part
122 * indicates what type of evaluation should be performed.
123 *
124 * @param string $displayCondition
125 * @param array $record
126 * @param string $flexformValueKey
127 * @return boolean
128 * @see match()
129 */
130 protected function matchSingle($displayCondition, array $record = array(), $flexformValueKey = '') {
131 $this->record = $record;
132 $this->flexformValueKey = $flexformValueKey;
133 $result = FALSE;
134 list($matchType, $condition) = explode(':', $displayCondition, 2);
135 switch ($matchType) {
136 case 'EXT':
137 $result = $this->matchExtensionCondition($condition);
138 break;
139 case 'FIELD':
140 $result = $this->matchFieldCondition($condition);
141 break;
142 case 'HIDE_FOR_NON_ADMINS':
143 $result = $this->matchHideForNonAdminsCondition();
144 break;
145 case 'HIDE_L10N_SIBLINGS':
146 $result = $this->matchHideL10nSiblingsCondition($condition);
147 break;
148 case 'REC':
149 $result = $this->matchRecordCondition($condition);
150 break;
151 case 'VERSION':
152 $result = $this->matchVersionCondition($condition);
153 break;
154 }
155 return $result;
156 }
157
158 /**
159 * Evaluates conditions concerning extensions
160 *
161 * Example:
162 * "EXT:saltedpasswords:LOADED:TRUE" => TRUE, if extension saltedpasswords is loaded.
163 *
164 * @param string $condition
165 * @return boolean
166 */
167 protected function matchExtensionCondition($condition) {
168 $result = FALSE;
169 list($extensionKey, $operator, $operand) = explode(':', $condition, 3);
170 if ($operator === 'LOADED') {
171 if (strtoupper($operand) === 'TRUE') {
172 $result = \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded($extensionKey);
173 } elseif (strtoupper($operand) === 'FALSE') {
174 $result = !\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded($extensionKey);
175 }
176 }
177 return $result;
178 }
179
180 /**
181 * Evaluates conditions concerning a field of the current record.
182 * Requires a record set via ->setRecord()
183 *
184 * Example:
185 * "FIELD:sys_language_uid:>:0" => TRUE, if the field 'sys_language_uid' is greater than 0
186 *
187 * @param string $condition
188 * @return boolean
189 */
190 protected function matchFieldCondition($condition) {
191 list($fieldName, $operator, $operand) = explode(':', $condition, 3);
192 if ($this->flexformValueKey) {
193 if (strpos($fieldName, 'parentRec.') !== FALSE) {
194 $fieldNameParts = explode('.', $fieldName, 2);
195 $fieldValue = $this->record['parentRec'][$fieldNameParts[1]];
196 } else {
197 $fieldValue = $this->record[$fieldName][$this->flexformValueKey];
198 }
199 } else {
200 $fieldValue = $this->record[$fieldName];
201 }
202
203 $result = FALSE;
204 switch ($operator) {
205 case 'REQ':
206 if (strtoupper($operand) === 'TRUE') {
207 $result = (bool) $fieldValue;
208 } else {
209 $result = !$fieldValue;
210 }
211 break;
212 case '>':
213 $result = $fieldValue > $operand;
214 break;
215 case '<':
216 $result = $fieldValue < $operand;
217 break;
218 case '>=':
219 $result = $fieldValue >= $operand;
220 break;
221 case '<=':
222 $result = $fieldValue <= $operand;
223 break;
224 case '-':
225 case '!-':
226 list($minimum, $maximum) = explode('-', $operand);
227 $result = $fieldValue >= $minimum && $fieldValue <= $maximum;
228 if ($operator{0} === '!') {
229 $result = !$result;
230 }
231 break;
232 case 'IN':
233 case '!IN':
234 case '=':
235 case '!=':
236 $result = \TYPO3\CMS\Core\Utility\GeneralUtility::inList($operand, $fieldValue);
237 if ($operator{0} === '!') {
238 $result = !$result;
239 }
240 break;
241 }
242 return $result;
243 }
244
245 /**
246 * Evaluates TRUE if current backend user is an admin.
247 *
248 * @return boolean
249 */
250 protected function matchHideForNonAdminsCondition() {
251 return (bool) $this->getBackendUser()->isAdmin();
252 }
253
254 /**
255 * Evaluates whether the field is a value for the default language.
256 * Works only for <langChildren>=1, otherwise it has no effect.
257 *
258 * @param string $condition
259 * @return boolean
260 */
261 protected function matchHideL10nSiblingsCondition($condition) {
262 $result = FALSE;
263 if ($this->flexformValueKey === 'vDEF') {
264 $result = TRUE;
265 } elseif ($condition === 'except_admin' && $this->getBackendUser()->isAdmin()) {
266 $result = TRUE;
267 }
268 return $result;
269 }
270
271 /**
272 * Evaluates conditions concerning the status of the current record.
273 * Requires a record set via ->setRecord()
274 *
275 * Example:
276 * "REC:NEW:FALSE" => TRUE, if the record is already persisted (has a uid > 0)
277 *
278 * @param string $condition
279 * @return boolean
280 */
281 protected function matchRecordCondition($condition) {
282 $result = FALSE;
283 list($operator, $operand) = explode(':', $condition, 2);
284 if ($operator === 'NEW') {
285 if (strtoupper($operand) === 'TRUE') {
286 $result = !(intval($this->record['uid']) > 0);
287 } elseif (strtoupper($operand) === 'FALSE') {
288 $result = (intval($this->record['uid']) > 0);
289 }
290 }
291 return $result;
292 }
293
294 /**
295 * Evaluates whether the current record is versioned.
296 * Requires a record set via ->setRecord()
297 *
298 * @param string $condition
299 * @return boolean
300 */
301 protected function matchVersionCondition($condition) {
302 $result = FALSE;
303 list($operator, $operand) = explode(':', $condition, 2);
304 if ($operator === 'IS') {
305 $isNewRecord = !(intval($this->record['uid']) > 0);
306 // Detection of version can be done be detecting the workspace of the user
307 $isUserInWorkspace = $this->getBackendUser()->workspace > 0;
308 if (intval($this->record['pid']) == -1 || intval($this->record['_ORIG_pid']) == -1) {
309 $isRecordDetectedAsVersion = TRUE;
310 } else {
311 $isRecordDetectedAsVersion = FALSE;
312 }
313 // New records in a workspace are not handled as a version record
314 // if it's no new version, we detect versions like this:
315 // -- if user is in workspace: always TRUE
316 // -- if editor is in live ws: only TRUE if pid == -1
317 $isVersion = ($isUserInWorkspace || $isRecordDetectedAsVersion) && !$isNewRecord;
318 if (strtoupper($operand) === 'TRUE') {
319 $result = $isVersion;
320 } elseif (strtoupper($operand) === 'FALSE') {
321 $result = !$isVersion;
322 }
323 }
324 return $result;
325 }
326
327 /**
328 * Get current backend user
329 *
330 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
331 */
332 protected function getBackendUser() {
333 return $GLOBALS['BE_USER'];
334 }
335 }
336
337 ?>