[FEATURE] Support literal strings in boolean arguments
[Packages/TYPO3.CMS.git] / typo3 / sysext / fluid / Classes / Core / Parser / SyntaxTree / BooleanNode.php
1 <?php
2 namespace TYPO3\CMS\Fluid\Core\Parser\SyntaxTree;
3
4 /* *
5 * This script is backported from the TYPO3 Flow package "TYPO3.Fluid". *
6 * *
7 * It is free software; you can redistribute it and/or modify it under *
8 * the terms of the GNU Lesser General Public License, either version 3 *
9 * of the License, or (at your option) any later version. *
10 * *
11 * The TYPO3 project - inspiring people to share! *
12 * */
13 /**
14 * A node which is used inside boolean arguments
15 */
16 class BooleanNode extends \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode {
17
18 /**
19 * List of comparators which are supported in the boolean expression language.
20 *
21 * Make sure that if one string is contained in one another, the longer
22 * string is listed BEFORE the shorter one.
23 * Example: put ">=" before ">"
24 *
25 * @var array
26 */
27 static protected $comparators = array('==', '!=', '%', '>=', '>', '<=', '<');
28
29 /**
30 * A regular expression which checks the text nodes of a boolean expression.
31 * Used to define how the regular expression language should look like.
32 *
33 * @var string
34 */
35 static protected $booleanExpressionTextNodeCheckerRegularExpression = '/
36 ^ # Start with first input symbol
37 (?: # start repeat
38 COMPARATORS # We allow all comparators
39 |\s* # Arbitary spaces
40 |-? # Numbers, possibly with the "minus" symbol in front.
41 [0-9]+ # some digits
42 (?: # and optionally a dot, followed by some more digits
43 \\.
44 [0-9]+
45 )?
46 |\'[^\'\\\\]* # single quoted string literals with possibly escaped single quotes
47 (?:
48 \\\\. # escaped character
49 [^\'\\\\]* # unrolled loop following Jeffrey E.F. Friedl
50 )*\'
51 |"[^"\\\\]* # double quoted string literals with possibly escaped double quotes
52 (?:
53 \\\\. # escaped character
54 [^"\\\\]* # unrolled loop following Jeffrey E.F. Friedl
55 )*"
56 )*
57 $/x';
58
59 /**
60 * Left side of the comparison
61 *
62 * @var \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode
63 */
64 protected $leftSide;
65
66 /**
67 * Right side of the comparison
68 *
69 * @var \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode
70 */
71 protected $rightSide;
72
73 /**
74 * The comparator. One element of self::$comparators. If NULL,
75 * no comparator was found, and self::$syntaxTreeNode should
76 * instead be evaluated.
77 *
78 * @var string
79 */
80 protected $comparator;
81
82 /**
83 * If no comparator was found, the syntax tree node should be
84 * converted to boolean.
85 *
86 * @var \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode
87 */
88 protected $syntaxTreeNode;
89
90 /**
91 * Constructor. Parses the syntax tree node and fills $this->leftSide, $this->rightSide,
92 * $this->comparator and $this->syntaxTreeNode.
93 *
94 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $syntaxTreeNode
95 * @throws \TYPO3\CMS\Fluid\Core\Parser\Exception
96 */
97 public function __construct(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode $syntaxTreeNode) {
98 $childNodes = $syntaxTreeNode->getChildNodes();
99 if (count($childNodes) > 3) {
100 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('A boolean expression has more than tree parts.', 1244201848);
101 } elseif (count($childNodes) === 0) {
102 // In this case, we do not have child nodes; i.e. the current SyntaxTreeNode
103 // is a text node with a literal comparison like "1 == 1"
104 $childNodes = array($syntaxTreeNode);
105 }
106 $this->leftSide = new \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\RootNode();
107 $this->rightSide = new \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\RootNode();
108 $this->comparator = NULL;
109 foreach ($childNodes as $childNode) {
110 if ($childNode instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode && !preg_match(str_replace('COMPARATORS', implode('|', self::$comparators), self::$booleanExpressionTextNodeCheckerRegularExpression), $childNode->getText())) {
111 // $childNode is text node, and no comparator found.
112 $this->comparator = NULL;
113 // skip loop and fall back to classical to boolean conversion.
114 break;
115 }
116 if ($this->comparator !== NULL) {
117 // comparator already set, we are evaluating the right side of the comparator
118 $this->rightSide->addChildNode($childNode);
119 } elseif ($childNode instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode && ($this->comparator = $this->getComparatorFromString($childNode->getText()))) {
120 // comparator in current string segment
121 $explodedString = explode($this->comparator, $childNode->getText());
122 if (isset($explodedString[0]) && trim($explodedString[0]) !== '') {
123 $value = trim($explodedString[0]);
124 if (is_numeric($value)) {
125 $this->leftSide->addChildNode(new \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\NumericNode($value));
126 } else {
127 $this->leftSide->addChildNode(new \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode(preg_replace('/(^[\'"]|[\'"]$)/', '', $value)));
128 }
129 }
130 if (isset($explodedString[1]) && trim($explodedString[1]) !== '') {
131 $value = trim($explodedString[1]);
132 if (is_numeric($value)) {
133 $this->rightSide->addChildNode(new \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\NumericNode($value));
134 } else {
135 $this->rightSide->addChildNode(new \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode(preg_replace('/(^[\'"]|[\'"]$)/', '', $value)));
136 }
137 }
138 } else {
139 // comparator not found yet, on the left side of the comparator
140 $this->leftSide->addChildNode($childNode);
141 }
142 }
143 if ($this->comparator === NULL) {
144 // No Comparator found, we need to evaluate the given syntax tree node manually
145 $this->syntaxTreeNode = $syntaxTreeNode;
146 }
147 }
148
149 /**
150 * @return string
151 * @internal
152 */
153 public function getComparator() {
154 return $this->comparator;
155 }
156
157 /**
158 * @return \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode
159 * @internal
160 */
161 public function getSyntaxTreeNode() {
162 return $this->syntaxTreeNode;
163 }
164
165 /**
166 * @return \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode
167 * @internal
168 */
169 public function getLeftSide() {
170 return $this->leftSide;
171 }
172
173 /**
174 * @return \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode
175 * @internal
176 */
177 public function getRightSide() {
178 return $this->rightSide;
179 }
180
181 /**
182 * @param \TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface $renderingContext
183 * @return boolean the boolean value
184 */
185 public function evaluate(\TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface $renderingContext) {
186 if ($this->comparator !== NULL) {
187 return self::evaluateComparator($this->comparator, $this->leftSide->evaluate($renderingContext), $this->rightSide->evaluate($renderingContext));
188 } else {
189 $value = $this->syntaxTreeNode->evaluate($renderingContext);
190 return self::convertToBoolean($value);
191 }
192 }
193
194 /**
195 * Do the actual comparison. Compares $leftSide and $rightSide with $comparator and emits a boolean value.
196 *
197 * Some special rules apply:
198 * - The == and != operators are comparing the Object Identity using === and !==, when one of the two
199 * operands are objects.
200 * - For arithmetic comparisons (%, >, >=, <, <=), some special rules apply:
201 * - arrays are only comparable with arrays, else the comparison yields FALSE
202 * - objects are only comparable with objects, else the comparison yields FALSE
203 * - the comparison is FALSE when two types are not comparable according to the table
204 * "Comparison with various types" on http://php.net/manual/en/language.operators.comparison.php
205 *
206 * This function must be static public, as it is also directly called from cached templates.
207 *
208 * @param string $comparator
209 * @param mixed $evaluatedLeftSide
210 * @param mixed $evaluatedRightSide
211 * @throws \TYPO3\CMS\Fluid\Core\Parser\Exception
212 * @return boolean TRUE if comparison of left and right side using the comparator emit TRUE, false otherwise
213 */
214 static public function evaluateComparator($comparator, $evaluatedLeftSide, $evaluatedRightSide) {
215 switch ($comparator) {
216 case '==':
217 if (is_object($evaluatedLeftSide) || is_object($evaluatedRightSide)) {
218 return $evaluatedLeftSide === $evaluatedRightSide;
219 } else {
220 return $evaluatedLeftSide == $evaluatedRightSide;
221 }
222 break;
223 case '!=':
224 if (is_object($evaluatedLeftSide) || is_object($evaluatedRightSide)) {
225 return $evaluatedLeftSide !== $evaluatedRightSide;
226 } else {
227 return $evaluatedLeftSide != $evaluatedRightSide;
228 }
229 break;
230 case '%':
231 if (!self::isComparable($evaluatedLeftSide, $evaluatedRightSide)) {
232 return FALSE;
233 }
234 return (boolean) ((integer) $evaluatedLeftSide % (integer) $evaluatedRightSide);
235 case '>':
236 if (!self::isComparable($evaluatedLeftSide, $evaluatedRightSide)) {
237 return FALSE;
238 }
239 return $evaluatedLeftSide > $evaluatedRightSide;
240 case '>=':
241 if (!self::isComparable($evaluatedLeftSide, $evaluatedRightSide)) {
242 return FALSE;
243 }
244 return $evaluatedLeftSide >= $evaluatedRightSide;
245 case '<':
246 if (!self::isComparable($evaluatedLeftSide, $evaluatedRightSide)) {
247 return FALSE;
248 }
249 return $evaluatedLeftSide < $evaluatedRightSide;
250 case '<=':
251 if (!self::isComparable($evaluatedLeftSide, $evaluatedRightSide)) {
252 return FALSE;
253 }
254 return $evaluatedLeftSide <= $evaluatedRightSide;
255 default:
256 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Comparator "' . $comparator . '" is not implemented.', 1244234398);
257 }
258 }
259
260 /**
261 * Checks whether two operands are comparable (based on their types). This implements
262 * the "Comparison with various types" table from http://php.net/manual/en/language.operators.comparison.php,
263 * only leaving out "array" with "anything" and "object" with anything; as we specify
264 * that arrays and objects are incomparable with anything else than their type.
265 *
266 * @param mixed $evaluatedLeftSide
267 * @param mixed $evaluatedRightSide
268 * @return boolean TRUE if the operands can be compared using arithmetic operators, FALSE otherwise.
269 */
270 static protected function isComparable($evaluatedLeftSide, $evaluatedRightSide) {
271 if ((is_null($evaluatedLeftSide) || is_string($evaluatedLeftSide)) && is_string($evaluatedRightSide)) {
272 return TRUE;
273 }
274 if (is_bool($evaluatedLeftSide) || is_null($evaluatedLeftSide)) {
275 return TRUE;
276 }
277 if (is_object($evaluatedLeftSide) && is_object($evaluatedRightSide)) {
278 return TRUE;
279 }
280 if ((is_string($evaluatedLeftSide) || is_resource($evaluatedLeftSide) || is_numeric($evaluatedLeftSide)) && (is_string($evaluatedRightSide) || is_resource($evaluatedRightSide) || is_numeric($evaluatedRightSide))) {
281 return TRUE;
282 }
283 if (is_array($evaluatedLeftSide) && is_array($evaluatedRightSide)) {
284 return TRUE;
285 }
286 return FALSE;
287 }
288
289 /**
290 * Determine if there is a comparator inside $string, and if yes, returns it.
291 *
292 * @param string $string string to check for a comparator inside
293 * @return string The comparator or NULL if none found.
294 */
295 protected function getComparatorFromString($string) {
296 foreach (self::$comparators as $comparator) {
297 if (strpos($string, $comparator) !== FALSE) {
298 return $comparator;
299 }
300 }
301 return NULL;
302 }
303
304 /**
305 * Convert argument strings to their equivalents. Needed to handle strings with a boolean meaning.
306 *
307 * Must be public and static as it is used from inside cached templates.
308 *
309 * @param mixed $value Value to be converted to boolean
310 * @return boolean
311 */
312 static public function convertToBoolean($value) {
313 if (is_bool($value)) {
314 return $value;
315 }
316 if (is_numeric($value)) {
317 return $value > 0;
318 }
319 if (is_string($value)) {
320 return !empty($value) && strtolower($value) !== 'false';
321 }
322 if (is_array($value) || is_object($value) && $value instanceof \Countable) {
323 return count($value) > 0;
324 }
325 if (is_object($value)) {
326 return TRUE;
327 }
328 return FALSE;
329 }
330 }
331
332 ?>