Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.57% |
505 / 534 |
|
89.23% |
58 / 65 |
CRAP | |
0.00% |
0 / 1 |
FilterEvaluator | |
94.57% |
505 / 534 |
|
89.23% |
58 / 65 |
187.31 | |
0.00% |
0 / 1 |
__construct | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
2.00 | |||
toggleConditionLimit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
raiseCondCount | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
setVariables | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCacheVersion | |
23.08% |
3 / 13 |
|
0.00% |
0 / 1 |
3.82 | |||
resetState | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
checkSyntaxThrow | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
checkSyntax | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
checkConditions | |
62.50% |
15 / 24 |
|
0.00% |
0 / 1 |
6.32 | |||
parse | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
evaluateExpression | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTree | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
1 | |||
evalTree | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getUsedVars | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
evalNode | |
99.36% |
155 / 156 |
|
0.00% |
0 / 1 |
58 | |||
callFunc | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
8 | |||
callKeyword | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
varExists | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getVarValue | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
setUserVariable | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
funcLc | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
funcUc | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
funcLen | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
funcSpecialRatio | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
funcCount | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
funcRCount | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
3 | |||
funcGetMatches | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
2 | |||
funcIPInRange | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
funcIPInRanges | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
funcCCNorm | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
funcSanitize | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
funcContainsAny | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
funcContainsAll | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
funcCCNormContainsAny | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
funcCCNormContainsAll | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
contains | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
7 | |||
funcEqualsToAny | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
equalsToAny | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
ccnorm | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
rmspecials | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
rmdoubles | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
rmwhitespace | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
funcRMSpecials | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
funcRMWhitespace | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
funcRMDoubles | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
funcNorm | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
funcSubstr | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
funcStrPos | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
funcStrReplace | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
funcStrReplaceRegexp | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
2 | |||
funcStrRegexEscape | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
funcSetVar | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
containmentKeyword | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
keywordIn | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
keywordContains | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
keywordLike | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
keywordRegex | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
keywordRegexInsensitive | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
castString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
castInt | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
castFloat | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
castBool | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
maybeDiscardNode | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
mungeRegexp | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
checkRegexMatchesEmpty | |
25.00% |
2 / 8 |
|
0.00% |
0 / 1 |
6.80 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter\Parser; |
4 | |
5 | use Exception; |
6 | use InvalidArgumentException; |
7 | use MediaWiki\Extension\AbuseFilter\KeywordsManager; |
8 | use MediaWiki\Extension\AbuseFilter\Parser\Exception\ConditionLimitException; |
9 | use MediaWiki\Extension\AbuseFilter\Parser\Exception\ExceptionBase; |
10 | use MediaWiki\Extension\AbuseFilter\Parser\Exception\InternalException; |
11 | use MediaWiki\Extension\AbuseFilter\Parser\Exception\UserVisibleException; |
12 | use MediaWiki\Extension\AbuseFilter\Parser\Exception\UserVisibleWarning; |
13 | use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder; |
14 | use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager; |
15 | use MediaWiki\Language\Language; |
16 | use MediaWiki\Parser\Sanitizer; |
17 | use Psr\Log\LoggerInterface; |
18 | use Wikimedia\Equivset\Equivset; |
19 | use Wikimedia\IPUtils; |
20 | use Wikimedia\ObjectCache\BagOStuff; |
21 | use Wikimedia\Stats\IBufferingStatsdDataFactory; |
22 | |
23 | /** |
24 | * This class evaluates an AST generated by the filter parser. |
25 | * |
26 | * @todo Override checkSyntax and make it only try to build the AST. That would mean faster results, |
27 | * and no need to mess with DUNDEFINED and the like. However, we must first try to reduce the |
28 | * amount of runtime-only exceptions, and try to detect them in the AFPTreeParser instead. |
29 | * Otherwise, people may be able to save a broken filter without the syntax check reporting that. |
30 | */ |
31 | class FilterEvaluator { |
32 | private const CACHE_VERSION = 1; |
33 | |
34 | public const FUNCTIONS = [ |
35 | 'lcase' => 'funcLc', |
36 | 'ucase' => 'funcUc', |
37 | 'length' => 'funcLen', |
38 | 'string' => 'castString', |
39 | 'int' => 'castInt', |
40 | 'float' => 'castFloat', |
41 | 'bool' => 'castBool', |
42 | 'norm' => 'funcNorm', |
43 | 'ccnorm' => 'funcCCNorm', |
44 | 'ccnorm_contains_any' => 'funcCCNormContainsAny', |
45 | 'ccnorm_contains_all' => 'funcCCNormContainsAll', |
46 | 'specialratio' => 'funcSpecialRatio', |
47 | 'rmspecials' => 'funcRMSpecials', |
48 | 'rmdoubles' => 'funcRMDoubles', |
49 | 'rmwhitespace' => 'funcRMWhitespace', |
50 | 'count' => 'funcCount', |
51 | 'rcount' => 'funcRCount', |
52 | 'get_matches' => 'funcGetMatches', |
53 | 'ip_in_range' => 'funcIPInRange', |
54 | 'ip_in_ranges' => 'funcIPInRanges', |
55 | 'contains_any' => 'funcContainsAny', |
56 | 'contains_all' => 'funcContainsAll', |
57 | 'equals_to_any' => 'funcEqualsToAny', |
58 | 'substr' => 'funcSubstr', |
59 | 'strlen' => 'funcLen', |
60 | 'strpos' => 'funcStrPos', |
61 | 'str_replace' => 'funcStrReplace', |
62 | 'str_replace_regexp' => 'funcStrReplaceRegexp', |
63 | 'rescape' => 'funcStrRegexEscape', |
64 | 'set' => 'funcSetVar', |
65 | 'set_var' => 'funcSetVar', |
66 | 'sanitize' => 'funcSanitize', |
67 | ]; |
68 | |
69 | /** |
70 | * The minimum and maximum amount of arguments required by each function. |
71 | * @var int[][] |
72 | */ |
73 | public const FUNC_ARG_COUNT = [ |
74 | 'lcase' => [ 1, 1 ], |
75 | 'ucase' => [ 1, 1 ], |
76 | 'length' => [ 1, 1 ], |
77 | 'string' => [ 1, 1 ], |
78 | 'int' => [ 1, 1 ], |
79 | 'float' => [ 1, 1 ], |
80 | 'bool' => [ 1, 1 ], |
81 | 'norm' => [ 1, 1 ], |
82 | 'ccnorm' => [ 1, 1 ], |
83 | 'ccnorm_contains_any' => [ 2, INF ], |
84 | 'ccnorm_contains_all' => [ 2, INF ], |
85 | 'specialratio' => [ 1, 1 ], |
86 | 'rmspecials' => [ 1, 1 ], |
87 | 'rmdoubles' => [ 1, 1 ], |
88 | 'rmwhitespace' => [ 1, 1 ], |
89 | 'count' => [ 1, 2 ], |
90 | 'rcount' => [ 1, 2 ], |
91 | 'get_matches' => [ 2, 2 ], |
92 | 'ip_in_range' => [ 2, 2 ], |
93 | 'ip_in_ranges' => [ 2, INF ], |
94 | 'contains_any' => [ 2, INF ], |
95 | 'contains_all' => [ 2, INF ], |
96 | 'equals_to_any' => [ 2, INF ], |
97 | 'substr' => [ 2, 3 ], |
98 | 'strlen' => [ 1, 1 ], |
99 | 'strpos' => [ 2, 3 ], |
100 | 'str_replace' => [ 3, 3 ], |
101 | 'str_replace_regexp' => [ 3, 3 ], |
102 | 'rescape' => [ 1, 1 ], |
103 | 'set' => [ 2, 2 ], |
104 | 'set_var' => [ 2, 2 ], |
105 | 'sanitize' => [ 1, 1 ], |
106 | ]; |
107 | |
108 | // Functions that affect parser state, and shouldn't be cached. |
109 | private const ACTIVE_FUNCTIONS = [ |
110 | 'funcSetVar', |
111 | ]; |
112 | |
113 | public const KEYWORDS = [ |
114 | 'in' => 'keywordIn', |
115 | 'like' => 'keywordLike', |
116 | 'matches' => 'keywordLike', |
117 | 'contains' => 'keywordContains', |
118 | 'rlike' => 'keywordRegex', |
119 | 'irlike' => 'keywordRegexInsensitive', |
120 | 'regex' => 'keywordRegex', |
121 | ]; |
122 | |
123 | /** |
124 | * @var bool Are we allowed to use short-circuit evaluation? |
125 | */ |
126 | private $mAllowShort; |
127 | |
128 | /** |
129 | * @var VariableHolder |
130 | */ |
131 | private $mVariables; |
132 | /** |
133 | * @var int The current amount of conditions being consumed |
134 | */ |
135 | private $mCondCount; |
136 | /** |
137 | * @var bool Whether the condition limit is enabled. |
138 | */ |
139 | private $condLimitEnabled = true; |
140 | /** |
141 | * @var string|null The ID of the filter being parsed, if available. Can also be "global-$ID" |
142 | */ |
143 | private $mFilter; |
144 | /** |
145 | * @var bool Whether we can allow retrieving _builtin_ variables not included in $this->mVariables |
146 | */ |
147 | private $allowMissingVariables = false; |
148 | |
149 | /** |
150 | * @var BagOStuff Used to cache the AST and the tokens |
151 | */ |
152 | private $cache; |
153 | /** |
154 | * @var bool Whether the AST was retrieved from cache |
155 | */ |
156 | private $fromCache = false; |
157 | /** |
158 | * @var LoggerInterface Used for debugging |
159 | */ |
160 | private $logger; |
161 | /** |
162 | * @var Language Content language, used for language-dependent functions |
163 | */ |
164 | private $contLang; |
165 | /** |
166 | * @var IBufferingStatsdDataFactory |
167 | */ |
168 | private $statsd; |
169 | |
170 | /** @var KeywordsManager */ |
171 | private $keywordsManager; |
172 | |
173 | /** @var VariablesManager */ |
174 | private $varManager; |
175 | |
176 | /** @var int */ |
177 | private $conditionsLimit; |
178 | |
179 | /** @var UserVisibleWarning[] */ |
180 | private $warnings = []; |
181 | |
182 | /** |
183 | * @var array Cached results of functions |
184 | */ |
185 | private $funcCache = []; |
186 | |
187 | /** |
188 | * @var Equivset |
189 | */ |
190 | private $equivset; |
191 | |
192 | /** |
193 | * @var array AFPToken::TID values found during node evaluation |
194 | */ |
195 | private $usedVars = []; |
196 | |
197 | /** |
198 | * Create a new instance |
199 | * |
200 | * @param Language $contLang Content language, used for language-dependent function |
201 | * @param BagOStuff $cache Used to cache the AST and the tokens |
202 | * @param LoggerInterface $logger Used for debugging |
203 | * @param KeywordsManager $keywordsManager |
204 | * @param VariablesManager $varManager |
205 | * @param IBufferingStatsdDataFactory $statsdDataFactory |
206 | * @param Equivset $equivset |
207 | * @param int $conditionsLimit |
208 | * @param VariableHolder|null $vars |
209 | */ |
210 | public function __construct( |
211 | Language $contLang, |
212 | BagOStuff $cache, |
213 | LoggerInterface $logger, |
214 | KeywordsManager $keywordsManager, |
215 | VariablesManager $varManager, |
216 | IBufferingStatsdDataFactory $statsdDataFactory, |
217 | Equivset $equivset, |
218 | int $conditionsLimit, |
219 | ?VariableHolder $vars = null |
220 | ) { |
221 | $this->contLang = $contLang; |
222 | $this->cache = $cache; |
223 | $this->logger = $logger; |
224 | $this->statsd = $statsdDataFactory; |
225 | $this->keywordsManager = $keywordsManager; |
226 | $this->varManager = $varManager; |
227 | $this->equivset = $equivset; |
228 | $this->conditionsLimit = $conditionsLimit; |
229 | $this->resetState(); |
230 | if ( $vars ) { |
231 | $this->mVariables = $vars; |
232 | } |
233 | } |
234 | |
235 | /** |
236 | * For use in batch scripts and the like |
237 | * |
238 | * @param bool $enable True to enable the limit, false to disable it |
239 | */ |
240 | public function toggleConditionLimit( $enable ) { |
241 | $this->condLimitEnabled = $enable; |
242 | } |
243 | |
244 | /** |
245 | * @throws ConditionLimitException |
246 | */ |
247 | private function raiseCondCount() { |
248 | $this->mCondCount++; |
249 | if ( $this->condLimitEnabled && $this->mCondCount > $this->conditionsLimit ) { |
250 | throw new ConditionLimitException(); |
251 | } |
252 | } |
253 | |
254 | /** |
255 | * @param VariableHolder $vars |
256 | */ |
257 | public function setVariables( VariableHolder $vars ) { |
258 | $this->mVariables = $vars; |
259 | } |
260 | |
261 | /** |
262 | * Return the generated version of the parser for cache invalidation |
263 | * purposes. Automatically tracks list of all functions and invalidates the |
264 | * cache if it is changed. |
265 | * @return string |
266 | */ |
267 | private static function getCacheVersion() { |
268 | static $version = null; |
269 | if ( $version !== null ) { |
270 | return $version; |
271 | } |
272 | |
273 | $versionKey = [ |
274 | self::CACHE_VERSION, |
275 | AFPTreeParser::CACHE_VERSION, |
276 | AbuseFilterTokenizer::CACHE_VERSION, |
277 | SyntaxChecker::CACHE_VERSION, |
278 | array_keys( self::FUNCTIONS ), |
279 | array_keys( self::KEYWORDS ), |
280 | ]; |
281 | $version = hash( 'sha256', serialize( $versionKey ) ); |
282 | |
283 | return $version; |
284 | } |
285 | |
286 | /** |
287 | * Resets the state of the parser |
288 | */ |
289 | private function resetState() { |
290 | $this->mVariables = new VariableHolder(); |
291 | $this->mCondCount = 0; |
292 | $this->mAllowShort = true; |
293 | $this->mFilter = null; |
294 | $this->warnings = []; |
295 | $this->usedVars = []; |
296 | } |
297 | |
298 | /** |
299 | * Check the syntax of $filter, throwing an exception if invalid |
300 | * @param string $filter |
301 | * @return true When successful |
302 | * @throws UserVisibleException |
303 | */ |
304 | public function checkSyntaxThrow( string $filter ): bool { |
305 | $this->allowMissingVariables = true; |
306 | $origAS = $this->mAllowShort; |
307 | try { |
308 | $this->mAllowShort = false; |
309 | $this->evalTree( $this->getTree( $filter ) ); |
310 | } finally { |
311 | $this->mAllowShort = $origAS; |
312 | $this->allowMissingVariables = false; |
313 | } |
314 | |
315 | return true; |
316 | } |
317 | |
318 | /** |
319 | * Check the syntax of $filter, without throwing |
320 | * |
321 | * @param string $filter |
322 | * @return ParserStatus |
323 | */ |
324 | public function checkSyntax( string $filter ): ParserStatus { |
325 | $initialConds = $this->mCondCount; |
326 | try { |
327 | $this->checkSyntaxThrow( $filter ); |
328 | } catch ( UserVisibleException $excep ) { |
329 | } |
330 | |
331 | return new ParserStatus( |
332 | $excep ?? null, |
333 | $this->warnings, |
334 | $this->mCondCount - $initialConds |
335 | ); |
336 | } |
337 | |
338 | /** |
339 | * This is the main entry point. It checks the given conditions and returns whether |
340 | * they match. Parser errors are always logged. |
341 | * |
342 | * @param string $conds |
343 | * @param string|null $filter The ID of the filter being parsed |
344 | * @return RuleCheckerStatus |
345 | */ |
346 | public function checkConditions( string $conds, $filter = null ): RuleCheckerStatus { |
347 | $this->mFilter = $filter; |
348 | $excep = null; |
349 | $initialConds = $this->mCondCount; |
350 | $startTime = microtime( true ); |
351 | try { |
352 | $res = $this->parse( $conds ); |
353 | } catch ( ExceptionBase $excep ) { |
354 | $res = false; |
355 | } |
356 | $this->statsd->timing( 'abusefilter_cachingParser_full', microtime( true ) - $startTime ); |
357 | $result = new RuleCheckerStatus( |
358 | $res, |
359 | $this->fromCache, |
360 | $excep, |
361 | $this->warnings, |
362 | $this->mCondCount - $initialConds |
363 | ); |
364 | |
365 | if ( $excep !== null ) { |
366 | if ( $excep instanceof UserVisibleException ) { |
367 | $msg = $excep->getMessageForLogs(); |
368 | } else { |
369 | $msg = $excep->getMessage(); |
370 | } |
371 | |
372 | $this->logger->warning( |
373 | "AbuseFilter parser error: {parser_error}", |
374 | [ 'parser_error' => $msg, 'broken_filter' => $filter ?: 'none' ] |
375 | ); |
376 | } |
377 | |
378 | return $result; |
379 | } |
380 | |
381 | /** |
382 | * @param string $code |
383 | * @return bool |
384 | */ |
385 | public function parse( $code ) { |
386 | $res = $this->evalTree( $this->getTree( $code ) ); |
387 | return $res->getType() === AFPData::DUNDEFINED ? false : $res->toBool(); |
388 | } |
389 | |
390 | /** |
391 | * @param string $filter |
392 | * @return mixed |
393 | */ |
394 | public function evaluateExpression( $filter ) { |
395 | return $this->evalTree( $this->getTree( $filter ) )->toNative(); |
396 | } |
397 | |
398 | /** |
399 | * @param string $code |
400 | * @return AFPSyntaxTree |
401 | */ |
402 | private function getTree( $code ): AFPSyntaxTree { |
403 | $this->fromCache = true; |
404 | return $this->cache->getWithSetCallback( |
405 | $this->cache->makeGlobalKey( |
406 | __CLASS__, |
407 | self::getCacheVersion(), |
408 | hash( 'sha256', $code ) |
409 | ), |
410 | BagOStuff::TTL_DAY, |
411 | function () use ( $code ) { |
412 | $this->fromCache = false; |
413 | $tokenizer = new AbuseFilterTokenizer( $this->cache ); |
414 | $tokens = $tokenizer->getTokens( $code ); |
415 | $parser = new AFPTreeParser( $this->logger, $this->statsd, $this->keywordsManager ); |
416 | $parser->setFilter( $this->mFilter ); |
417 | $tree = $parser->parse( $tokens ); |
418 | $checker = new SyntaxChecker( |
419 | $tree, |
420 | $this->keywordsManager, |
421 | SyntaxChecker::MCONSERVATIVE, |
422 | false |
423 | ); |
424 | $checker->start(); |
425 | return $tree; |
426 | } |
427 | ); |
428 | } |
429 | |
430 | /** |
431 | * @param AFPSyntaxTree $tree |
432 | * @return AFPData |
433 | */ |
434 | private function evalTree( AFPSyntaxTree $tree ): AFPData { |
435 | $startTime = microtime( true ); |
436 | $root = $tree->getRoot(); |
437 | |
438 | if ( !$root ) { |
439 | return new AFPData( AFPData::DNULL ); |
440 | } |
441 | |
442 | $ret = $this->evalNode( $root ); |
443 | $this->statsd->timing( 'abusefilter_cachingParser_eval', microtime( true ) - $startTime ); |
444 | return $ret; |
445 | } |
446 | |
447 | /** |
448 | * Parse a filter and return the variables used. |
449 | * All variables are AFPToken::TID and are found during the node stepthrough in evaluation |
450 | * and saved to self::usedVars to be returned to the caller in this function. |
451 | * |
452 | * @param string $filter |
453 | * @return string[] |
454 | */ |
455 | public function getUsedVars( string $filter ): array { |
456 | $this->checkSyntax( $filter ); |
457 | return array_unique( $this->usedVars ); |
458 | } |
459 | |
460 | /** |
461 | * Evaluate the value of the specified AST node. |
462 | * |
463 | * @param AFPTreeNode $node The node to evaluate. |
464 | * @return AFPData|AFPTreeNode|string |
465 | * @throws ExceptionBase |
466 | * @throws UserVisibleException |
467 | */ |
468 | private function evalNode( AFPTreeNode $node ) { |
469 | switch ( $node->type ) { |
470 | case AFPTreeNode::ATOM: |
471 | $tok = $node->children; |
472 | switch ( $tok->type ) { |
473 | case AFPToken::TID: |
474 | return $this->getVarValue( strtolower( $tok->value ) ); |
475 | case AFPToken::TSTRING: |
476 | return new AFPData( AFPData::DSTRING, $tok->value ); |
477 | case AFPToken::TFLOAT: |
478 | return new AFPData( AFPData::DFLOAT, $tok->value ); |
479 | case AFPToken::TINT: |
480 | return new AFPData( AFPData::DINT, $tok->value ); |
481 | /** @noinspection PhpMissingBreakStatementInspection */ |
482 | case AFPToken::TKEYWORD: |
483 | switch ( $tok->value ) { |
484 | case "true": |
485 | return new AFPData( AFPData::DBOOL, true ); |
486 | case "false": |
487 | return new AFPData( AFPData::DBOOL, false ); |
488 | case "null": |
489 | return new AFPData( AFPData::DNULL ); |
490 | } |
491 | // Fallthrough intended |
492 | default: |
493 | // @codeCoverageIgnoreStart |
494 | throw new InternalException( "Unknown token provided in the ATOM node" ); |
495 | // @codeCoverageIgnoreEnd |
496 | } |
497 | // Unreachable line |
498 | case AFPTreeNode::ARRAY_DEFINITION: |
499 | $items = []; |
500 | // Foreach is usually faster than array_map |
501 | // @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach children is array here |
502 | foreach ( $node->children as $el ) { |
503 | $items[] = $this->evalNode( $el ); |
504 | } |
505 | return new AFPData( AFPData::DARRAY, $items ); |
506 | |
507 | case AFPTreeNode::FUNCTION_CALL: |
508 | $functionName = $node->children[0]; |
509 | $args = array_slice( $node->children, 1 ); |
510 | |
511 | $dataArgs = []; |
512 | // Foreach is usually faster than array_map |
513 | foreach ( $args as $arg ) { |
514 | $dataArgs[] = $this->evalNode( $arg ); |
515 | } |
516 | |
517 | return $this->callFunc( $functionName, $dataArgs, $node->position ); |
518 | case AFPTreeNode::ARRAY_INDEX: |
519 | [ $array, $offset ] = $node->children; |
520 | |
521 | $array = $this->evalNode( $array ); |
522 | // Note: we MUST evaluate the offset to ensure it is valid, regardless |
523 | // of $array! |
524 | $offset = $this->evalNode( $offset ); |
525 | // @todo If $array has no elements we could already throw an outofbounds. We don't |
526 | // know what the index is, though. |
527 | if ( $offset->getType() === AFPData::DUNDEFINED ) { |
528 | return new AFPData( AFPData::DUNDEFINED ); |
529 | } |
530 | $offset = $offset->toInt(); |
531 | |
532 | if ( $array->getType() === AFPData::DUNDEFINED ) { |
533 | return new AFPData( AFPData::DUNDEFINED ); |
534 | } |
535 | |
536 | if ( $array->getType() !== AFPData::DARRAY ) { |
537 | throw new UserVisibleException( 'notarray', $node->position, [] ); |
538 | } |
539 | |
540 | $array = $array->toArray(); |
541 | if ( count( $array ) <= $offset ) { |
542 | throw new UserVisibleException( 'outofbounds', $node->position, |
543 | [ $offset, count( $array ) ] ); |
544 | } elseif ( $offset < 0 ) { |
545 | throw new UserVisibleException( 'negativeindex', $node->position, [ $offset ] ); |
546 | } |
547 | |
548 | return $array[$offset]; |
549 | |
550 | case AFPTreeNode::UNARY: |
551 | [ $operation, $argument ] = $node->children; |
552 | $argument = $this->evalNode( $argument ); |
553 | if ( $operation === '-' ) { |
554 | return $argument->unaryMinus(); |
555 | } |
556 | return $argument; |
557 | |
558 | case AFPTreeNode::KEYWORD_OPERATOR: |
559 | [ $keyword, $leftOperand, $rightOperand ] = $node->children; |
560 | $leftOperand = $this->evalNode( $leftOperand ); |
561 | $rightOperand = $this->evalNode( $rightOperand ); |
562 | |
563 | return $this->callKeyword( $keyword, $leftOperand, $rightOperand, $node->position ); |
564 | case AFPTreeNode::BOOL_INVERT: |
565 | [ $argument ] = $node->children; |
566 | $argument = $this->evalNode( $argument ); |
567 | return $argument->boolInvert(); |
568 | |
569 | case AFPTreeNode::POW: |
570 | [ $base, $exponent ] = $node->children; |
571 | $base = $this->evalNode( $base ); |
572 | $exponent = $this->evalNode( $exponent ); |
573 | return $base->pow( $exponent ); |
574 | |