Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.49% covered (success)
99.49%
195 / 196
95.65% covered (success)
95.65%
22 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
AFPData
99.49% covered (success)
99.49%
195 / 196
95.65% covered (success)
95.65%
22 / 23
129
0.00% covered (danger)
0.00%
0 / 1
 getType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 newFromPHPVar
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
9
 castTypes
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
15
 boolInvert
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 pow
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 equals
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
21
 unaryMinus
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 boolOp
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
8
 compareOp
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
12
 mulRel
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
12
 sum
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
8
 sub
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 hasUndefined
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 cloneAsUndefinedReplacedWithNull
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 toNative
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
10
 toBool
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toFloat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toInt
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toNumber
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 toArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\Parser;
4
5use InvalidArgumentException;
6use MediaWiki\Extension\AbuseFilter\Parser\Exception\InternalException;
7use MediaWiki\Extension\AbuseFilter\Parser\Exception\UserVisibleException;
8use RuntimeException;
9
10class 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}