Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
99.49% |
195 / 196 |
|
95.65% |
22 / 23 |
CRAP | |
0.00% |
0 / 1 |
AFPData | |
99.49% |
195 / 196 |
|
95.65% |
22 / 23 |
129 | |
0.00% |
0 / 1 |
getType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
newFromPHPVar | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
9 | |||
castTypes | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
15 | |||
boolInvert | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
pow | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
equals | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
21 | |||
unaryMinus | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
boolOp | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
8 | |||
compareOp | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
12 | |||
mulRel | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
12 | |||
sum | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
8 | |||
sub | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
hasUndefined | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 | |||
cloneAsUndefinedReplacedWithNull | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
toNative | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
10 | |||
toBool | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
toFloat | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
toInt | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
toNumber | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
toArray | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter\Parser; |
4 | |
5 | use InvalidArgumentException; |
6 | use MediaWiki\Extension\AbuseFilter\Parser\Exception\InternalException; |
7 | use MediaWiki\Extension\AbuseFilter\Parser\Exception\UserVisibleException; |
8 | use RuntimeException; |
9 | |
10 | class AFPData { |
11 | // Datatypes |
12 | public const DINT = 'int'; |
13 | public const DSTRING = 'string'; |
14 | public const DNULL = 'null'; |
15 | public const DBOOL = 'bool'; |
16 | public const DFLOAT = 'float'; |
17 | public const DARRAY = 'array'; |
18 | // Special purpose type for non-initialized stuff |
19 | public const DUNDEFINED = 'undefined'; |
20 | |
21 | /** |
22 | * Translation table mapping shell-style wildcards to PCRE equivalents. |
23 | * Derived from <http://www.php.net/manual/en/function.fnmatch.php#100207> |
24 | * @internal |
25 | */ |
26 | public const WILDCARD_MAP = [ |
27 | '\*' => '.*', |
28 | '\+' => '\+', |
29 | '\-' => '\-', |
30 | '\.' => '\.', |
31 | '\?' => '.', |
32 | '\[' => '[', |
33 | '\[\!' => '[^', |
34 | '\\' => '\\\\', |
35 | '\]' => ']', |
36 | ]; |
37 | |
38 | /** |
39 | * @var string One of the D* const from this class |
40 | * @internal Use $this->getType() instead |
41 | */ |
42 | public $type; |
43 | /** |
44 | * @var mixed|null|AFPData[] The actual data contained in this object |
45 | * @internal Use $this->getData() instead |
46 | */ |
47 | public $data; |
48 | |
49 | /** |
50 | * @return string |
51 | */ |
52 | public function getType() { |
53 | return $this->type; |
54 | } |
55 | |
56 | /** |
57 | * @return AFPData[]|mixed|null |
58 | */ |
59 | public function getData() { |
60 | return $this->data; |
61 | } |
62 | |
63 | /** |
64 | * @param string $type |
65 | * @param AFPData[]|mixed|null $val |
66 | */ |
67 | public function __construct( $type, $val = null ) { |
68 | if ( $type === self::DUNDEFINED && $val !== null ) { |
69 | // Sanity |
70 | throw new InvalidArgumentException( 'DUNDEFINED cannot have a non-null value' ); |
71 | } |
72 | $this->type = $type; |
73 | $this->data = $val; |
74 | } |
75 | |
76 | /** |
77 | * @param mixed $var |
78 | * @return AFPData |
79 | * @throws InternalException |
80 | */ |
81 | public static function newFromPHPVar( $var ) { |
82 | switch ( gettype( $var ) ) { |
83 | case 'string': |
84 | return new AFPData( self::DSTRING, $var ); |
85 | case 'integer': |
86 | return new AFPData( self::DINT, $var ); |
87 | case 'double': |
88 | return new AFPData( self::DFLOAT, $var ); |
89 | case 'boolean': |
90 | return new AFPData( self::DBOOL, $var ); |
91 | case 'array': |
92 | $result = []; |
93 | foreach ( $var as $item ) { |
94 | $result[] = self::newFromPHPVar( $item ); |
95 | } |
96 | return new AFPData( self::DARRAY, $result ); |
97 | case 'NULL': |
98 | return new AFPData( self::DNULL ); |
99 | default: |
100 | throw new InternalException( |
101 | 'Data type ' . get_debug_type( $var ) . ' is not supported by AbuseFilter' |
102 | ); |
103 | } |
104 | } |
105 | |
106 | /** |
107 | * @param AFPData $orig |
108 | * @param string $target |
109 | * @return AFPData |
110 | */ |
111 | public static function castTypes( AFPData $orig, $target ) { |
112 | if ( $orig->type === $target ) { |
113 | return $orig; |
114 | } |
115 | if ( $orig->type === self::DUNDEFINED ) { |
116 | // This case should be handled at a higher level, to avoid implicitly relying on what |
117 | // this method will do for the specific case. |
118 | throw new InternalException( 'Refusing to cast DUNDEFINED to something else' ); |
119 | } |
120 | if ( $target === self::DNULL ) { |
121 | // We don't expose any method to cast to null. And, actually, should we? |
122 | return new AFPData( self::DNULL ); |
123 | } |
124 | |
125 | if ( $orig->type === self::DARRAY ) { |
126 | if ( $target === self::DBOOL ) { |
127 | return new AFPData( self::DBOOL, (bool)count( $orig->data ) ); |
128 | } elseif ( $target === self::DFLOAT ) { |
129 | return new AFPData( self::DFLOAT, floatval( count( $orig->data ) ) ); |
130 | } elseif ( $target === self::DINT ) { |
131 | return new AFPData( self::DINT, count( $orig->data ) ); |
132 | } elseif ( $target === self::DSTRING ) { |
133 | $s = ''; |
134 | foreach ( $orig->data as $item ) { |
135 | $s .= $item->toString() . "\n"; |
136 | } |
137 | |
138 | return new AFPData( self::DSTRING, $s ); |
139 | } |
140 | } |
141 | |
142 | if ( $target === self::DBOOL ) { |
143 | return new AFPData( self::DBOOL, (bool)$orig->data ); |
144 | } elseif ( $target === self::DFLOAT ) { |
145 | return new AFPData( self::DFLOAT, floatval( $orig->data ) ); |
146 | } elseif ( $target === self::DINT ) { |
147 | return new AFPData( self::DINT, intval( $orig->data ) ); |
148 | } elseif ( $target === self::DSTRING ) { |
149 | return new AFPData( self::DSTRING, strval( $orig->data ) ); |
150 | } elseif ( $target === self::DARRAY ) { |
151 | // We don't expose any method to cast to array |
152 | return new AFPData( self::DARRAY, [ $orig ] ); |
153 | } |
154 | throw new InternalException( 'Cannot cast ' . $orig->type . " to $target." ); |
155 | } |
156 | |
157 | /** |
158 | * @return AFPData |
159 | */ |
160 | public function boolInvert() { |
161 | if ( $this->type === self::DUNDEFINED ) { |
162 | return new AFPData( self::DUNDEFINED ); |
163 | } |
164 | return new AFPData( self::DBOOL, !$this->toBool() ); |
165 | } |
166 | |
167 | /** |
168 | * @param AFPData $exponent |
169 | * @return AFPData |
170 | */ |
171 | public function pow( AFPData $exponent ) { |
172 | if ( $this->type === self::DUNDEFINED || $exponent->type === self::DUNDEFINED ) { |
173 | return new AFPData( self::DUNDEFINED ); |
174 | } |
175 | $res = pow( $this->toNumber(), $exponent->toNumber() ); |
176 | $type = is_int( $res ) ? self::DINT : self::DFLOAT; |
177 | |
178 | return new AFPData( $type, $res ); |
179 | } |
180 | |
181 | /** |
182 | * @param AFPData $d2 |
183 | * @param bool $strict whether to also check types |
184 | * @return bool |
185 | * @throws InternalException if $this or $d2 is a DUNDEFINED. This shouldn't happen, because this method |
186 | * only returns a boolean, and thus the type of the result has already been decided and cannot |
187 | * be changed to be a DUNDEFINED from here. |
188 | * @internal |
189 | */ |
190 | public function equals( AFPData $d2, $strict = false ) { |
191 | if ( $this->type === self::DUNDEFINED || $d2->type === self::DUNDEFINED ) { |
192 | throw new InternalException( |
193 | __METHOD__ . " got a DUNDEFINED. This should be handled at a higher level" |
194 | ); |
195 | } elseif ( $this->type !== self::DARRAY && $d2->type !== self::DARRAY ) { |
196 | $typecheck = $this->type === $d2->type || !$strict; |
197 | return $typecheck && $this->toString() === $d2->toString(); |
198 | } elseif ( $this->type === self::DARRAY && $d2->type === self::DARRAY ) { |
199 | $data1 = $this->data; |
200 | $data2 = $d2->data; |
201 | if ( count( $data1 ) !== count( $data2 ) ) { |
202 | return false; |
203 | } |
204 | $length = count( $data1 ); |
205 | for ( $i = 0; $i < $length; $i++ ) { |
206 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable Array type |
207 | if ( $data1[$i]->equals( $data2[$i], $strict ) === false ) { |
208 | return false; |
209 | } |
210 | } |
211 | return true; |
212 | } else { |
213 | // Trying to compare an array to something else |
214 | if ( $strict ) { |
215 | return false; |
216 | } |
217 | if ( $this->type === self::DARRAY && count( $this->data ) === 0 ) { |
218 | return ( $d2->type === self::DBOOL && $d2->toBool() === false ) || $d2->type === self::DNULL; |
219 | } elseif ( $d2->type === self::DARRAY && count( $d2->data ) === 0 ) { |
220 | return ( $this->type === self::DBOOL && $this->toBool() === false ) || |
221 | $this->type === self::DNULL; |
222 | } else { |
223 | return false; |
224 | } |
225 | } |
226 | } |
227 | |
228 | /** |
229 | * @return AFPData |
230 | */ |
231 | public function unaryMinus() { |
232 | if ( $this->type === self::DUNDEFINED ) { |
233 | return new AFPData( self::DUNDEFINED ); |
234 | } elseif ( $this->type === self::DINT ) { |
235 | return new AFPData( $this->type, -$this->toInt() ); |
236 | } else { |
237 | return new AFPData( $this->type, -$this->toFloat() ); |
238 | } |
239 | } |
240 | |
241 | /** |
242 | * @param AFPData $b |
243 | * @param string $op |
244 | * @return AFPData |
245 | * @throws InternalException |
246 | */ |
247 | public function boolOp( AFPData $b, $op ) { |
248 | $a = $this->type === self::DUNDEFINED ? false : $this->toBool(); |
249 | $b = $b->type === self::DUNDEFINED ? false : $b->toBool(); |
250 | |
251 | if ( $op === '|' ) { |
252 | return new AFPData( self::DBOOL, $a || $b ); |
253 | } elseif ( $op === '&' ) { |
254 | return new AFPData( self::DBOOL, $a && $b ); |
255 | } elseif ( $op === '^' ) { |
256 | return new AFPData( self::DBOOL, $a xor $b ); |
257 | } |
258 | // Should never happen. |
259 | // @codeCoverageIgnoreStart |
260 | throw new InternalException( "Invalid boolean operation: {$op}" ); |
261 | // @codeCoverageIgnoreEnd |
262 | } |
263 | |
264 | /** |
265 | * @param AFPData $b |
266 | * @param string $op |
267 | * @return AFPData |
268 | * @throws InternalException |
269 | */ |
270 | public function compareOp( AFPData $b, $op ) { |
271 | if ( $this->type === self::DUNDEFINED || $b->type === self::DUNDEFINED ) { |
272 | return new AFPData( self::DUNDEFINED ); |
273 | } |
274 | if ( $op === '==' || $op === '=' ) { |
275 | return new AFPData( self::DBOOL, $this->equals( $b ) ); |
276 | } elseif ( $op === '!=' ) { |
277 | return new AFPData( self::DBOOL, !$this->equals( $b ) ); |
278 | } elseif ( $op === '===' ) { |
279 | return new AFPData( self::DBOOL, $this->equals( $b, true ) ); |
280 | } elseif ( $op === '!==' ) { |
281 | return new AFPData( self::DBOOL, !$this->equals( $b, true ) ); |
282 | } |
283 | |
284 | $a = $this->toString(); |
285 | $b = $b->toString(); |
286 | if ( $op === '>' ) { |
287 | return new AFPData( self::DBOOL, $a > $b ); |
288 | } elseif ( $op === '<' ) { |
289 | return new AFPData( self::DBOOL, $a < $b ); |
290 | } elseif ( $op === '>=' ) { |
291 | return new AFPData( self::DBOOL, $a >= $b ); |
292 | } elseif ( $op === '<=' ) { |
293 | return new AFPData( self::DBOOL, $a <= $b ); |
294 | } |
295 | // Should never happen |
296 | // @codeCoverageIgnoreStart |
297 | throw new InternalException( "Invalid comparison operation: {$op}" ); |
298 | // @codeCoverageIgnoreEnd |
299 | } |
300 | |
301 | /** |
302 | * @param AFPData $b |
303 | * @param string $op |
304 | * @param int $pos |
305 | * @return AFPData |
306 | * @throws UserVisibleException |
307 | * @throws InternalException |
308 | */ |
309 | public function mulRel( AFPData $b, $op, $pos ) { |
310 | if ( $b->type === self::DUNDEFINED ) { |
311 | // The LHS type is checked later, because we first need to ensure we're not |
312 | // dividing or taking modulo by 0 (and that should throw regardless of whether |
313 | // the LHS is undefined). |
314 | return new AFPData( self::DUNDEFINED ); |
315 | } |
316 | |
317 | $b = $b->toNumber(); |
318 | |
319 | if ( |
320 | ( $op === '/' && (float)$b === 0.0 ) || |
321 | ( $op === '%' && (int)$b === 0 ) |
322 | ) { |
323 | $lhs = $this->type === self::DUNDEFINED ? 0 : $this->toNumber(); |
324 | throw new UserVisibleException( 'dividebyzero', $pos, [ $lhs ] ); |
325 | } |
326 | |
327 | if ( $this->type === self::DUNDEFINED ) { |
328 | return new AFPData( self::DUNDEFINED ); |
329 | } |
330 | $a = $this->toNumber(); |
331 | |
332 | if ( $op === '*' ) { |
333 | $data = $a * $b; |
334 | } elseif ( $op === '/' ) { |
335 | $data = $a / $b; |
336 | } elseif ( $op === '%' ) { |
337 | $data = (int)$a % (int)$b; |
338 | } else { |
339 | // Should never happen |
340 | // @codeCoverageIgnoreStart |
341 | throw new InternalException( "Invalid multiplication-related operation: {$op}" ); |
342 | // @codeCoverageIgnoreEnd |
343 | } |
344 | |
345 | $type = is_int( $data ) ? self::DINT : self::DFLOAT; |
346 | |
347 | return new AFPData( $type, $data ); |
348 | } |
349 | |
350 | /** |
351 | * @param AFPData $b |
352 | * @return AFPData |
353 | */ |
354 | public function sum( AFPData $b ) { |
355 | if ( $this->type === self::DUNDEFINED || $b->type === self::DUNDEFINED ) { |
356 | return new AFPData( self::DUNDEFINED ); |
357 | } elseif ( $this->type === self::DSTRING || $b->type === self::DSTRING ) { |
358 | return new AFPData( self::DSTRING, $this->toString() . $b->toString() ); |
359 | } elseif ( $this->type === self::DARRAY && $b->type === self::DARRAY ) { |
360 | return new AFPData( self::DARRAY, array_merge( $this->toArray(), $b->toArray() ) ); |
361 | } else { |
362 | $res = $this->toNumber() + $b->toNumber(); |
363 | $type = is_int( $res ) ? self::DINT : self::DFLOAT; |
364 | |
365 | return new AFPData( $type, $res ); |
366 | } |
367 | } |
368 | |
369 | /** |
370 | * @param AFPData $b |
371 | * @return AFPData |
372 | */ |
373 | public function sub( AFPData $b ) { |
374 | if ( $this->type === self::DUNDEFINED || $b->type === self::DUNDEFINED ) { |
375 | return new AFPData( self::DUNDEFINED ); |
376 | } |
377 | $res = $this->toNumber() - $b->toNumber(); |
378 | $type = is_int( $res ) ? self::DINT : self::DFLOAT; |
379 | |
380 | return new AFPData( $type, $res ); |
381 | } |
382 | |
383 | /** |
384 | * Check whether this instance contains the DUNDEFINED type, recursively |
385 | * @return bool |
386 | */ |
387 | public function hasUndefined(): bool { |
388 | if ( $this->type === self::DUNDEFINED ) { |
389 | return true; |
390 | } |
391 | if ( $this->type === self::DARRAY ) { |
392 | foreach ( $this->data as $el ) { |
393 | if ( $el->hasUndefined() ) { |
394 | return true; |
395 | } |
396 | } |
397 | } |
398 | return false; |
399 | } |
400 | |
401 | /** |
402 | * Return a clone of this instance where DUNDEFINED is replaced with DNULL |
403 | * @return $this |
404 | */ |
405 | public function cloneAsUndefinedReplacedWithNull(): self { |
406 | if ( $this->type === self::DUNDEFINED ) { |
407 | return new self( self::DNULL ); |
408 | } |
409 | if ( $this->type === self::DARRAY ) { |
410 | $data = []; |
411 | foreach ( $this->data as $el ) { |
412 | $data[] = $el->cloneAsUndefinedReplacedWithNull(); |
413 | } |
414 | return new self( self::DARRAY, $data ); |
415 | } |
416 | return clone $this; |
417 | } |
418 | |
419 | /** Convert shorteners */ |
420 | |
421 | /** |
422 | * @throws RuntimeException |
423 | * @return mixed |
424 | */ |
425 | public function toNative() { |
426 | switch ( $this->type ) { |
427 | case self::DBOOL: |
428 | return $this->toBool(); |
429 | case self::DSTRING: |
430 | return $this->toString(); |
431 | case self::DFLOAT: |
432 | return $this->toFloat(); |
433 | case self::DINT: |
434 | return $this->toInt(); |
435 | case self::DARRAY: |
436 | $input = $this->toArray(); |
437 | $output = []; |
438 | foreach ( $input as $item ) { |
439 | $output[] = $item->toNative(); |
440 | } |
441 | |
442 | return $output; |
443 | case self::DNULL: |
444 | case self::DUNDEFINED: |
445 | return null; |
446 | default: |
447 | // @codeCoverageIgnoreStart |
448 | throw new RuntimeException( "Unknown type" ); |
449 | // @codeCoverageIgnoreEnd |
450 | } |
451 | } |
452 | |
453 | /** |
454 | * @return bool |
455 | */ |
456 | public function toBool() { |
457 | return self::castTypes( $this, self::DBOOL )->data; |
458 | } |
459 | |
460 | /** |
461 | * @return string |
462 | */ |
463 | public function toString() { |
464 | return self::castTypes( $this, self::DSTRING )->data; |
465 | } |
466 | |
467 | /** |
468 | * @return float |
469 | */ |
470 | public function toFloat() { |
471 | return self::castTypes( $this, self::DFLOAT )->data; |
472 | } |
473 | |
474 | /** |
475 | * @return int |
476 | */ |
477 | public function toInt() { |
478 | return self::castTypes( $this, self::DINT )->data; |
479 | } |
480 | |
481 | /** |
482 | * @return int|float |
483 | */ |
484 | public function toNumber() { |
485 | // Types that can be cast to int |
486 | $intLikeTypes = [ |
487 | self::DINT, |
488 | self::DBOOL, |
489 | self::DNULL |
490 | ]; |
491 | return in_array( $this->type, $intLikeTypes, true ) ? $this->toInt() : $this->toFloat(); |
492 | } |
493 | |
494 | /** |
495 | * @return array |
496 | */ |
497 | public function toArray() { |
498 | return self::castTypes( $this, self::DARRAY )->data; |
499 | } |
500 | } |