1
|
|
|
<?php |
2
|
|
|
namespace TYPO3Fluid\Fluid\Core\Parser; |
3
|
|
|
|
4
|
|
|
/* |
5
|
|
|
* This file belongs to the package "TYPO3 Fluid". |
6
|
|
|
* See LICENSE.txt that was shipped with this package. |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* This BooleanParser helps to parse and evaluate boolean expressions. |
11
|
|
|
* it's basically a recursive decent parser that uses a tokenizing regex |
12
|
|
|
* to walk a given expression while evaluating each step along the way. |
13
|
|
|
* |
14
|
|
|
* For a basic recursive decent exampel check out: |
15
|
|
|
* http://stackoverflow.com/questions/2093138/what-is-the-algorithm-for-parsing-expressions-in-infix-notation |
16
|
|
|
* |
17
|
|
|
* Parsingtree: |
18
|
|
|
* |
19
|
|
|
* evaluate/compile: start the whole cycle |
20
|
|
|
* parseOrToken: takes care of "||" parts |
21
|
|
|
* evaluateOr: evaluate the "||" part if found |
22
|
|
|
* parseAndToken: take care of "&&" parts |
23
|
|
|
* evaluateAnd: evaluate "&&" part if found |
24
|
|
|
* parseCompareToken: takes care any comparisons "==,!=,>,<,..." |
25
|
|
|
* evaluateCompare: evaluate the comparison if found |
26
|
|
|
* parseNotToken: takes care of any "!" negations |
27
|
|
|
* evaluateNot: evaluate the negation if found |
28
|
|
|
* parseBracketToken: takes care of any '()' parts and restarts the cycle |
29
|
|
|
* parseStringToken: takes care of any strings |
30
|
|
|
* evaluateTerm: evaluate terms from true/false/numeric/context |
31
|
|
|
* |
32
|
|
|
*/ |
33
|
|
|
class BooleanParser |
34
|
|
|
{ |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* List of comparators to check in the parseCompareToken if the current |
38
|
|
|
* part of the expression is a comparator and needs to be compared |
39
|
|
|
*/ |
40
|
|
|
const COMPARATORS = '==,===,!==,!=,<=,>=,<,>,%'; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Regex to parse a expression into tokens |
44
|
|
|
*/ |
45
|
|
|
const TOKENREGEX = '/ |
46
|
|
|
\s*( |
47
|
|
|
\\\\\' |
48
|
|
|
| |
49
|
|
|
\\" |
50
|
|
|
| |
51
|
|
|
[\'"] |
52
|
|
|
| |
53
|
|
|
[A-Za-z0-9\.\{\}\-\\\\]+ |
54
|
|
|
| |
55
|
|
|
\=\=\= |
56
|
|
|
| |
57
|
|
|
\=\= |
58
|
|
|
| |
59
|
|
|
!\=\= |
60
|
|
|
| |
61
|
|
|
!\= |
62
|
|
|
| |
63
|
|
|
<\= |
64
|
|
|
| |
65
|
|
|
>\= |
66
|
|
|
| |
67
|
|
|
< |
68
|
|
|
| |
69
|
|
|
> |
70
|
|
|
| |
71
|
|
|
% |
72
|
|
|
| |
73
|
|
|
\|\| |
74
|
|
|
| |
75
|
|
|
[aA][nN][dD] |
76
|
|
|
| |
77
|
|
|
&& |
78
|
|
|
| |
79
|
|
|
[oO][rR] |
80
|
|
|
| |
81
|
|
|
.? |
82
|
|
|
)\s* |
83
|
|
|
/xsu'; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Cursor that contains a integer value pointing to the location inside the |
87
|
|
|
* expression string that is used by the peek function to look for the part of |
88
|
|
|
* the expression that needs to be focused on next. This cursor is changed |
89
|
|
|
* by the consume method, by "consuming" part of the expression. |
90
|
|
|
* |
91
|
|
|
* @var integer |
92
|
|
|
*/ |
93
|
|
|
protected $cursor = 0; |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Expression that is parsed through peek and consume methods |
97
|
|
|
* |
98
|
|
|
* @var string |
99
|
|
|
*/ |
100
|
|
|
protected $expression; |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Context containing all variables that are references in the expression |
104
|
|
|
* |
105
|
|
|
* @var array |
106
|
|
|
*/ |
107
|
|
|
protected $context; |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Switch to enable compiling |
111
|
|
|
* |
112
|
|
|
* @var boolean |
113
|
|
|
*/ |
114
|
|
|
protected $compileToCode = false; |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Evaluate a expression to a boolean |
118
|
|
|
* |
119
|
|
|
* @param string $expression to be parsed |
120
|
|
|
* @param array $context containing variables that can be used in the expression |
121
|
|
|
* @return boolean |
122
|
|
|
*/ |
123
|
|
|
public function evaluate($expression, $context) |
124
|
|
|
{ |
125
|
|
|
$this->context = $context; |
126
|
|
|
$this->expression = $expression; |
127
|
|
|
$this->cursor = 0; |
128
|
|
|
return $this->parseOrToken(); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Parse and compile an expression into an php equivalent |
133
|
|
|
* |
134
|
|
|
* @param string $expression to be parsed |
135
|
|
|
* @return string |
136
|
|
|
*/ |
137
|
|
|
public function compile($expression) |
138
|
|
|
{ |
139
|
|
|
$this->expression = $expression; |
140
|
|
|
$this->cursor = 0; |
141
|
|
|
$this->compileToCode = true; |
142
|
|
|
return $this->parseOrToken(); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* The part of the expression we're currently focusing on based on the |
147
|
|
|
* tokenizing regex offset by the internally tracked cursor. |
148
|
|
|
* |
149
|
|
|
* @param boolean $includeWhitespace return surrounding whitespace with token |
150
|
|
|
* @return string |
151
|
|
|
*/ |
152
|
|
|
protected function peek($includeWhitespace = false) |
153
|
|
|
{ |
154
|
|
|
preg_match(static::TOKENREGEX, mb_substr($this->expression, $this->cursor), $matches); |
155
|
|
|
if ($includeWhitespace === true) { |
156
|
|
|
return $matches[0]; |
157
|
|
|
} |
158
|
|
|
return $matches[1]; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Consume part of the current expression by setting the internal cursor |
163
|
|
|
* to the position of the string in the expression and it's length |
164
|
|
|
* |
165
|
|
|
* @param string $string |
166
|
|
|
* @return void |
167
|
|
|
*/ |
168
|
|
|
protected function consume($string) |
169
|
|
|
{ |
170
|
|
|
if (mb_strlen($string) === 0) { |
171
|
|
|
return; |
172
|
|
|
} |
173
|
|
|
$this->cursor = mb_strpos($this->expression, $string, $this->cursor) + mb_strlen($string); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Passes the torch down to the next deeper parsing leve (and) |
178
|
|
|
* and checks then if there's a "or" expression that needs to be handled |
179
|
|
|
* |
180
|
|
|
* @return mixed |
181
|
|
|
*/ |
182
|
|
View Code Duplication |
protected function parseOrToken() |
|
|
|
|
183
|
|
|
{ |
184
|
|
|
$x = $this->parseAndToken(); |
185
|
|
|
while (($token = $this->peek()) && in_array(strtolower($token), ['||', 'or'])) { |
186
|
|
|
$this->consume($token); |
187
|
|
|
$y = $this->parseAndToken(); |
188
|
|
|
|
189
|
|
|
if ($this->compileToCode === true) { |
190
|
|
|
$x = '(' . $x . ' || ' . $y . ')'; |
191
|
|
|
continue; |
192
|
|
|
} |
193
|
|
|
$x = $this->evaluateOr($x, $y); |
194
|
|
|
} |
195
|
|
|
return $x; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* Passes the torch down to the next deeper parsing leve (compare) |
200
|
|
|
* and checks then if there's a "and" expression that needs to be handled |
201
|
|
|
* |
202
|
|
|
* @return mixed |
203
|
|
|
*/ |
204
|
|
View Code Duplication |
protected function parseAndToken() |
|
|
|
|
205
|
|
|
{ |
206
|
|
|
$x = $this->parseCompareToken(); |
207
|
|
|
while (($token = $this->peek()) && in_array(strtolower($token), ['&&', 'and'])) { |
208
|
|
|
$this->consume($token); |
209
|
|
|
$y = $this->parseCompareToken(); |
210
|
|
|
|
211
|
|
|
if ($this->compileToCode === true) { |
212
|
|
|
$x = '(' . $x . ' && ' . $y . ')'; |
213
|
|
|
continue; |
214
|
|
|
} |
215
|
|
|
$x = $this->evaluateAnd($x, $y); |
216
|
|
|
} |
217
|
|
|
return $x; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Passes the torch down to the next deeper parsing leven (not) |
222
|
|
|
* and checks then if there's a "compare" expression that needs to be handled |
223
|
|
|
* |
224
|
|
|
* @return mixed |
225
|
|
|
*/ |
226
|
|
|
protected function parseCompareToken() |
227
|
|
|
{ |
228
|
|
|
$x = $this->parseNotToken(); |
229
|
|
|
while (in_array($comparator = $this->peek(), explode(',', static::COMPARATORS))) { |
230
|
|
|
$this->consume($comparator); |
231
|
|
|
$y = $this->parseNotToken(); |
232
|
|
|
$x = $this->evaluateCompare($x, $y, $comparator); |
233
|
|
|
} |
234
|
|
|
return $x; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Check if we have encountered an not expression or pass the torch down |
239
|
|
|
* to the simpleToken method. |
240
|
|
|
* |
241
|
|
|
* @return mixed |
242
|
|
|
*/ |
243
|
|
|
protected function parseNotToken() |
244
|
|
|
{ |
245
|
|
|
if ($this->peek() === '!') { |
246
|
|
|
$this->consume('!'); |
247
|
|
|
$x = $this->parseNotToken(); |
248
|
|
|
|
249
|
|
|
if ($this->compileToCode === true) { |
250
|
|
|
return '!(' . $x . ')'; |
251
|
|
|
} |
252
|
|
|
return $this->evaluateNot($x); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
return $this->parseBracketToken(); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Takes care of restarting the whole parsing loop if it encounters a "(" or ")" |
260
|
|
|
* token or pass the torch down to the parseStringToken method |
261
|
|
|
* |
262
|
|
|
* @return mixed |
263
|
|
|
*/ |
264
|
|
|
protected function parseBracketToken() |
265
|
|
|
{ |
266
|
|
|
$t = $this->peek(); |
267
|
|
|
if ($t === '(') { |
268
|
|
|
$this->consume('('); |
269
|
|
|
$result = $this->parseOrToken(); |
270
|
|
|
$this->consume(')'); |
271
|
|
|
return $result; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
return $this->parseStringToken(); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Takes care of consuming pure string including whitespace or passes the torch |
279
|
|
|
* down to the parseTermToken method |
280
|
|
|
* |
281
|
|
|
* @return mixed |
282
|
|
|
*/ |
283
|
|
|
protected function parseStringToken() |
284
|
|
|
{ |
285
|
|
|
$t = $this->peek(); |
286
|
|
|
if ($t === '\'' || $t === '"') { |
287
|
|
|
$stringIdentifier = $t; |
288
|
|
|
$string = $stringIdentifier; |
289
|
|
|
$this->consume($stringIdentifier); |
290
|
|
|
while (trim($t = $this->peek(true)) !== $stringIdentifier) { |
291
|
|
|
$this->consume($t); |
292
|
|
|
$string .= $t; |
293
|
|
|
} |
294
|
|
|
$this->consume($stringIdentifier); |
295
|
|
|
$string .= $stringIdentifier; |
296
|
|
|
if ($this->compileToCode === true) { |
297
|
|
|
return $string; |
298
|
|
|
} |
299
|
|
|
return $this->evaluateTerm($string, $this->context); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
return $this->parseTermToken(); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* Takes care of restarting the whole parsing loop if it encounters a "(" or ")" |
307
|
|
|
* token, consumes a pure string including whitespace or passes the torch |
308
|
|
|
* down to the evaluateTerm method |
309
|
|
|
* |
310
|
|
|
* @return mixed |
311
|
|
|
*/ |
312
|
|
|
protected function parseTermToken() |
313
|
|
|
{ |
314
|
|
|
$t = $this->peek(); |
315
|
|
|
$this->consume($t); |
316
|
|
|
return $this->evaluateTerm($t, $this->context); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* Evaluate an "and" comparison |
321
|
|
|
* |
322
|
|
|
* @param mixed $x |
323
|
|
|
* @param mixed $y |
324
|
|
|
* @return boolean |
325
|
|
|
*/ |
326
|
|
|
protected function evaluateAnd($x, $y) |
327
|
|
|
{ |
328
|
|
|
return $x && $y; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
/** |
332
|
|
|
* Evaluate an "or" comparison |
333
|
|
|
* |
334
|
|
|
* @param mixed $x |
335
|
|
|
* @param mixed $y |
336
|
|
|
* @return boolean |
337
|
|
|
*/ |
338
|
|
|
protected function evaluateOr($x, $y) |
339
|
|
|
{ |
340
|
|
|
return $x || $y; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Evaluate an "not" comparison |
345
|
|
|
* |
346
|
|
|
* @param mixed $x |
347
|
|
|
* @return boolean|string |
348
|
|
|
*/ |
349
|
|
|
protected function evaluateNot($x) |
350
|
|
|
{ |
351
|
|
|
return !$x; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* Compare two variables based on a specified comparator |
356
|
|
|
* |
357
|
|
|
* @param mixed $x |
358
|
|
|
* @param mixed $y |
359
|
|
|
* @param string $comparator |
360
|
|
|
* @return boolean|string |
361
|
|
|
*/ |
362
|
|
|
protected function evaluateCompare($x, $y, $comparator) |
363
|
|
|
{ |
364
|
|
|
// enfore strong comparison for comparing two objects |
365
|
|
|
if ($comparator == '==' && is_object($x) && is_object($y)) { |
366
|
|
|
$comparator = '==='; |
367
|
|
|
} |
368
|
|
|
if ($comparator == '!=' && is_object($x) && is_object($y)) { |
369
|
|
|
$comparator = '!=='; |
370
|
|
|
} |
371
|
|
|
|
372
|
|
|
if ($this->compileToCode === true) { |
373
|
|
|
return sprintf('(%s %s %s)', $x, $comparator, $y); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
switch ($comparator) { |
377
|
|
|
case '==': |
378
|
|
|
$x = ($x == $y); |
379
|
|
|
break; |
380
|
|
|
|
381
|
|
|
case '===': |
382
|
|
|
$x = ($x === $y); |
383
|
|
|
break; |
384
|
|
|
|
385
|
|
|
case '!=': |
386
|
|
|
$x = ($x != $y); |
387
|
|
|
break; |
388
|
|
|
|
389
|
|
|
case '!==': |
390
|
|
|
$x = ($x !== $y); |
391
|
|
|
break; |
392
|
|
|
|
393
|
|
|
case '<=': |
394
|
|
|
$x = ($x <= $y); |
395
|
|
|
break; |
396
|
|
|
|
397
|
|
|
case '>=': |
398
|
|
|
$x = ($x >= $y); |
399
|
|
|
break; |
400
|
|
|
|
401
|
|
|
case '<': |
402
|
|
|
$x = ($x < $y); |
403
|
|
|
break; |
404
|
|
|
|
405
|
|
|
case '>': |
406
|
|
|
$x = ($x > $y); |
407
|
|
|
break; |
408
|
|
|
|
409
|
|
|
case '%': |
410
|
|
|
$x = ($x % $y); |
411
|
|
|
break; |
412
|
|
|
} |
413
|
|
|
return $x; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/** |
417
|
|
|
* Takes care of fetching terms from the context, converting to float/int, |
418
|
|
|
* converting true/false keywords into boolean or trim the final string of |
419
|
|
|
* quotation marks |
420
|
|
|
* |
421
|
|
|
* @param string $x |
422
|
|
|
* @param array $context |
423
|
|
|
* @return mixed |
424
|
|
|
*/ |
425
|
|
|
protected function evaluateTerm($x, $context) |
426
|
|
|
{ |
427
|
|
|
if (isset($context[$x]) || (mb_strpos($x, '{') === 0 && mb_substr($x, -1) === '}')) { |
428
|
|
|
if ($this->compileToCode === true) { |
429
|
|
|
return BooleanParser::class . '::convertNodeToBoolean($context["' . trim($x, '{}') . '"])'; |
430
|
|
|
} |
431
|
|
|
return self::convertNodeToBoolean($context[trim($x, '{}')]); |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
if (is_numeric($x)) { |
435
|
|
|
if ($this->compileToCode === true) { |
436
|
|
|
return $x; |
437
|
|
|
} |
438
|
|
|
if (mb_strpos($x, '.') !== false) { |
439
|
|
|
return (float)$x; |
440
|
|
|
} else { |
441
|
|
|
return (int)$x; |
442
|
|
|
} |
443
|
|
|
} |
444
|
|
|
|
445
|
|
View Code Duplication |
if (trim(strtolower($x)) === 'true') { |
|
|
|
|
446
|
|
|
if ($this->compileToCode === true) { |
447
|
|
|
return 'TRUE'; |
448
|
|
|
} |
449
|
|
|
return true; |
450
|
|
|
} |
451
|
|
View Code Duplication |
if (trim(strtolower($x)) === 'false') { |
|
|
|
|
452
|
|
|
if ($this->compileToCode === true) { |
453
|
|
|
return 'FALSE'; |
454
|
|
|
} |
455
|
|
|
return false; |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
if ($this->compileToCode === true) { |
459
|
|
|
return '"' . trim($x, '\'"') . '"'; |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
return trim($x, '\'"'); |
463
|
|
|
} |
464
|
|
|
|
465
|
|
|
public static function convertNodeToBoolean($value) { |
466
|
|
|
if (is_object($value) && $value instanceof \Countable) { |
467
|
|
|
return count($value) > 0; |
468
|
|
|
} |
469
|
|
|
return $value; |
470
|
|
|
} |
471
|
|
|
} |
472
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.