Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.46% covered (success)
99.46%
185 / 186
95.65% covered (success)
95.65%
22 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
AFPData
99.46% covered (success)
99.46%
185 / 186
95.65% covered (success)
95.65%
22 / 23
120
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
1
 boolOp
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 compareOp
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 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%
11 / 11
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|self[] 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 self[]|mixed|null
58     */
59    public function getData() {
60        return $this->data;
61    }
62
63    /**
64     * @param string $type
65     * @param self[]|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 self
79     * @throws InternalException
80     */
81    public static function newFromPHPVar( $var ) {
82        switch ( gettype( $var ) ) {
83            case 'string':
84                return new self( self::DSTRING, $var );
85            case 'integer':
86                return new self( self::DINT, $var );
87            case 'double':
88                return new self( self::DFLOAT, $var );
89            case 'boolean':
90                return new self( self::DBOOL, $var );
91            case 'array':
92                $result = [];
93                foreach ( $var as $item ) {
94                    $result[] = self::newFromPHPVar( $item );
95                }
96                return new self( self::DARRAY, $result );
97            case 'NULL':
98                return new self( 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 self $orig
108     * @param string $target
109     * @return self
110     */
111    public static function castTypes( self $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 self( self::DNULL );
123        }
124
125        if ( $orig->type === self::DARRAY ) {
126            if ( $target === self::DBOOL ) {
127                return new self( self::DBOOL, (bool)count( $orig->data ) );
128            } elseif ( $target === self::DFLOAT ) {
129                return new self( self::DFLOAT, floatval( count( $orig->data ) ) );
130            } elseif ( $target === self::DINT ) {
131                return new self( 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 self( self::DSTRING, $s );
139            }
140        }
141
142        if ( $target === self::DBOOL ) {
143            return new self( self::DBOOL, (bool)$orig->data );
144        } elseif ( $target === self::DFLOAT ) {
145            return new self( self::DFLOAT, floatval( $orig->data ) );
146        } elseif ( $target === self::DINT ) {
147            return new self( self::DINT, intval( $orig->data ) );
148        } elseif ( $target === self::DSTRING ) {
149            return new self( self::DSTRING, strval( $orig->data ) );
150        } elseif ( $target === self::DARRAY ) {
151            // We don't expose any method to cast to array
152            return new self( self::DARRAY, [ $orig ] );
153        }
154        throw new InternalException( 'Cannot cast ' . $orig->type . " to $target." );
155    }
156
157    /**
158     * @return self
159     */
160    public function boolInvert() {
161        if ( $this->type === self::DUNDEFINED ) {
162            return new self( self::DUNDEFINED );
163        }
164        return new self( self::DBOOL, !$this->toBool() );
165    }
166
167    /**
168     * @param self $exponent
169     * @return self
170     */
171    public function pow( self $exponent ) {
172        if ( $this->type === self::DUNDEFINED || $exponent->type === self::DUNDEFINED ) {
173            return new self( self::DUNDEFINED );
174        }
175        $res = pow( $this->toNumber(), $exponent->toNumber() );
176        $type = is_int( $res ) ? self::DINT : self::DFLOAT;
177
178        return new self( $type, $res );
179    }
180
181    /**
182     * @param self $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( self $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 self
230     */
231    public function unaryMinus() {
232        return match ( $this->type ) {
233            self::DUNDEFINED => new self( self::DUNDEFINED ),
234            self::DINT => new self( $this->type, -$this->toInt() ),
235            default => new self( $this->type, -$this->toFloat() ),
236        };
237    }
238
239    /**
240     * @param self $b
241     * @param string $op
242     * @return self
243     * @throws InternalException
244     */
245    public function boolOp( self $b, $op ) {
246        $a = $this->type === self::DUNDEFINED ? false : $this->toBool();
247        $b = $b->type === self::DUNDEFINED ? false : $b->toBool();
248
249        return match ( $op ) {
250            '|' => new self( self::DBOOL, $a || $b ),
251            '&' => new self( self::DBOOL, $a && $b ),
252            '^' => new self( self::DBOOL, $a xor $b ),
253            // Should never happen.
254            // @codeCoverageIgnoreStart
255            default => throw new InternalException( "Invalid boolean operation: {$op}" ),
256            // @codeCoverageIgnoreEnd
257        };
258    }
259
260    /**
261     * @param self $b
262     * @param string $op
263     * @return self
264     * @throws InternalException
265     */
266    public function compareOp( self $b, $op ) {
267        if ( $this->type === self::DUNDEFINED || $b->type === self::DUNDEFINED ) {
268            return new self( self::DUNDEFINED );
269        }
270        if ( $op === '==' || $op === '=' ) {
271            return new self( self::DBOOL, $this->equals( $b ) );
272        } elseif ( $op === '!=' ) {
273            return new self( self::DBOOL, !$this->equals( $b ) );
274        } elseif ( $op === '===' ) {
275            return new self( self::DBOOL, $this->equals( $b, true ) );
276        } elseif ( $op === '!==' ) {
277            return new self( self::DBOOL, !$this->equals( $b, true ) );
278        }
279
280        $a = $this->toString();
281        $b = $b->toString();
282        return match ( $op ) {
283            '>' => new self( self::DBOOL, $a > $b ),
284            '<' => new self( self::DBOOL, $a < $b ),
285            '>=' => new self( self::DBOOL, $a >= $b ),
286            '<=' => new self( self::DBOOL, $a <= $b ),
287            // Should never happen
288            // @codeCoverageIgnoreStart
289            default => throw new InternalException( "Invalid comparison operation: $op" )
290            // @codeCoverageIgnoreEnd
291        };
292    }
293
294    /**
295     * @param self $b
296     * @param string $op
297     * @param int $pos
298     * @return self
299     * @throws UserVisibleException
300     * @throws InternalException
301     */
302    public function mulRel( self $b, $op, $pos ) {
303        if ( $b->type === self::DUNDEFINED ) {
304            // The LHS type is checked later, because we first need to ensure we're not
305            // dividing or taking modulo by 0 (and that should throw regardless of whether
306            // the LHS is undefined).
307            return new self( self::DUNDEFINED );
308        }
309
310        $b = $b->toNumber();
311
312        if (
313            ( $op === '/' && (float)$b === 0.0 ) ||
314            ( $op === '%' && (int)$b === 0 )
315        ) {
316            $lhs = $this->type === self::DUNDEFINED ? 0 : $this->toNumber();
317            throw new UserVisibleException( 'dividebyzero', $pos, [ $lhs ] );
318        }
319
320        if ( $this->type === self::DUNDEFINED ) {
321            return new self( self::DUNDEFINED );
322        }
323        $a = $this->toNumber();
324
325        if ( $op === '*' ) {
326            $data = $a * $b;
327        } elseif ( $op === '/' ) {
328            $data = $a / $b;
329        } elseif ( $op === '%' ) {
330            $data = (int)$a % (int)$b;
331        } else {
332            // Should never happen
333            // @codeCoverageIgnoreStart
334            throw new InternalException( "Invalid multiplication-related operation: {$op}" );
335            // @codeCoverageIgnoreEnd
336        }
337
338        $type = is_int( $data ) ? self::DINT : self::DFLOAT;
339
340        return new self( $type, $data );
341    }
342
343    /**
344     * @param self $b
345     * @return self
346     */
347    public function sum( self $b ) {
348        if ( $this->type === self::DUNDEFINED || $b->type === self::DUNDEFINED ) {
349            return new self( self::DUNDEFINED );
350        } elseif ( $this->type === self::DSTRING || $b->type === self::DSTRING ) {
351            return new self( self::DSTRING, $this->toString() . $b->toString() );
352        } elseif ( $this->type === self::DARRAY && $b->type === self::DARRAY ) {
353            return new self( self::DARRAY, array_merge( $this->toArray(), $b->toArray() ) );
354        } else {
355            $res = $this->toNumber() + $b->toNumber();
356            $type = is_int( $res ) ? self::DINT : self::DFLOAT;
357
358            return new self( $type, $res );
359        }
360    }
361
362    /**
363     * @param self $b
364     * @return self
365     */
366    public function sub( self $b ) {
367        if ( $this->type === self::DUNDEFINED || $b->type === self::DUNDEFINED ) {
368            return new self( self::DUNDEFINED );
369        }
370        $res = $this->toNumber() - $b->toNumber();
371        $type = is_int( $res ) ? self::DINT : self::DFLOAT;
372
373        return new self( $type, $res );
374    }
375
376    /**
377     * Check whether this instance contains the DUNDEFINED type, recursively
378     */
379    public function hasUndefined(): bool {
380        if ( $this->type === self::DUNDEFINED ) {
381            return true;
382        }
383        if ( $this->type === self::DARRAY ) {
384            foreach ( $this->data as $el ) {
385                if ( $el->hasUndefined() ) {
386                    return true;
387                }
388            }
389        }
390        return false;
391    }
392
393    /**
394     * Return a clone of this instance where DUNDEFINED is replaced with DNULL
395     * @return $this
396     */
397    public function cloneAsUndefinedReplacedWithNull(): self {
398        if ( $this->type === self::DUNDEFINED ) {
399            return new self( self::DNULL );
400        }
401        if ( $this->type === self::DARRAY ) {
402            $data = [];
403            foreach ( $this->data as $el ) {
404                $data[] = $el->cloneAsUndefinedReplacedWithNull();
405            }
406            return new self( self::DARRAY, $data );
407        }
408        return clone $this;
409    }
410
411    /** Convert shorteners */
412
413    /**
414     * @throws RuntimeException
415     * @return mixed
416     */
417    public function toNative() {
418        switch ( $this->type ) {
419            case self::DBOOL:
420                return $this->toBool();
421            case self::DSTRING:
422                return $this->toString();
423            case self::DFLOAT:
424                return $this->toFloat();
425            case self::DINT:
426                return $this->toInt();
427            case self::DARRAY:
428                $input = $this->toArray();
429                $output = [];
430                foreach ( $input as $item ) {
431                    $output[] = $item->toNative();
432                }
433
434                return $output;
435            case self::DNULL:
436            case self::DUNDEFINED:
437                return null;
438            default:
439                // @codeCoverageIgnoreStart
440                throw new RuntimeException( "Unknown type" );
441                // @codeCoverageIgnoreEnd
442        }
443    }
444
445    /**
446     * @return bool
447     */
448    public function toBool() {
449        return self::castTypes( $this, self::DBOOL )->data;
450    }
451
452    /**
453     * @return string
454     */
455    public function toString() {
456        return self::castTypes( $this, self::DSTRING )->data;
457    }
458
459    /**
460     * @return float
461     */
462    public function toFloat() {
463        return self::castTypes( $this, self::DFLOAT )->data;
464    }
465
466    /**
467     * @return int
468     */
469    public function toInt() {
470        return self::castTypes( $this, self::DINT )->data;
471    }
472
473    /**
474     * @return int|float
475     */
476    public function toNumber() {
477        // Types that can be cast to int
478        $intLikeTypes = [
479            self::DINT,
480            self::DBOOL,
481            self::DNULL
482        ];
483        return in_array( $this->type, $intLikeTypes, true ) ? $this->toInt() : $this->toFloat();
484    }
485
486    /**
487     * @return array
488     */
489    public function toArray() {
490        return self::castTypes( $this, self::DARRAY )->data;
491    }
492}