Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.47% covered (success)
99.47%
188 / 189
95.65% covered (success)
95.65%
22 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
AFPData
99.47% covered (success)
99.47%
188 / 189
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%
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        if ( $this->type === self::DUNDEFINED ) {
233            return new self( self::DUNDEFINED );
234        } elseif ( $this->type === self::DINT ) {
235            return new self( $this->type, -$this->toInt() );
236        } else {
237            return new self( $this->type, -$this->toFloat() );
238        }
239    }
240
241    /**
242     * @param self $b
243     * @param string $op
244     * @return self
245     * @throws InternalException
246     */
247    public function boolOp( self $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 self( self::DBOOL, $a || $b );
253        } elseif ( $op === '&' ) {
254            return new self( self::DBOOL, $a && $b );
255        } elseif ( $op === '^' ) {
256            return new self( 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 self $b
266     * @param string $op
267     * @return self
268     * @throws InternalException
269     */
270    public function compareOp( self $b, $op ) {
271        if ( $this->type === self::DUNDEFINED || $b->type === self::DUNDEFINED ) {
272            return new self( self::DUNDEFINED );
273        }
274        if ( $op === '==' || $op === '=' ) {
275            return new self( self::DBOOL, $this->equals( $b ) );
276        } elseif ( $op === '!=' ) {
277            return new self( self::DBOOL, !$this->equals( $b ) );
278        } elseif ( $op === '===' ) {
279            return new self( self::DBOOL, $this->equals( $b, true ) );
280        } elseif ( $op === '!==' ) {
281            return new self( self::DBOOL, !$this->equals( $b, true ) );
282        }
283
284        $a = $this->toString();
285        $b = $b->toString();
286        if ( $op === '>' ) {
287            return new self( self::DBOOL, $a > $b );
288        } elseif ( $op === '<' ) {
289            return new self( self::DBOOL, $a < $b );
290        } elseif ( $op === '>=' ) {
291            return new self( self::DBOOL, $a >= $b );
292        } elseif ( $op === '<=' ) {
293            return new self( 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 self $b
303     * @param string $op
304     * @param int $pos
305     * @return self
306     * @throws UserVisibleException
307     * @throws InternalException
308     */
309    public function mulRel( self $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 self( 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 self( 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 self( $type, $data );
348    }
349
350    /**
351     * @param self $b
352     * @return self
353     */
354    public function sum( self $b ) {
355        if ( $this->type === self::DUNDEFINED || $b->type === self::DUNDEFINED ) {
356            return new self( self::DUNDEFINED );
357        } elseif ( $this->type === self::DSTRING || $b->type === self::DSTRING ) {
358            return new self( self::DSTRING, $this->toString() . $b->toString() );
359        } elseif ( $this->type === self::DARRAY && $b->type === self::DARRAY ) {
360            return new self( 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 self( $type, $res );
366        }
367    }
368
369    /**
370     * @param self $b
371     * @return self
372     */
373    public function sub( self $b ) {
374        if ( $this->type === self::DUNDEFINED || $b->type === self::DUNDEFINED ) {
375            return new self( self::DUNDEFINED );
376        }
377        $res = $this->toNumber() - $b->toNumber();
378        $type = is_int( $res ) ? self::DINT : self::DFLOAT;
379
380        return new self( $type, $res );
381    }
382
383    /**
384     * Check whether this instance contains the DUNDEFINED type, recursively
385     */
386    public function hasUndefined(): bool {
387        if ( $this->type === self::DUNDEFINED ) {
388            return true;
389        }
390        if ( $this->type === self::DARRAY ) {
391            foreach ( $this->data as $el ) {
392                if ( $el->hasUndefined() ) {
393                    return true;
394                }
395            }
396        }
397        return false;
398    }
399
400    /**
401     * Return a clone of this instance where DUNDEFINED is replaced with DNULL
402     * @return $this
403     */
404    public function cloneAsUndefinedReplacedWithNull(): self {
405        if ( $this->type === self::DUNDEFINED ) {
406            return new self( self::DNULL );
407        }
408        if ( $this->type === self::DARRAY ) {
409            $data = [];
410            foreach ( $this->data as $el ) {
411                $data[] = $el->cloneAsUndefinedReplacedWithNull();
412            }
413            return new self( self::DARRAY, $data );
414        }
415        return clone $this;
416    }
417
418    /** Convert shorteners */
419
420    /**
421     * @throws RuntimeException
422     * @return mixed
423     */
424    public function toNative() {
425        switch ( $this->type ) {
426            case self::DBOOL:
427                return $this->toBool();
428            case self::DSTRING:
429                return $this->toString();
430            case self::DFLOAT:
431                return $this->toFloat();
432            case self::DINT:
433                return $this->toInt();
434            case self::DARRAY:
435                $input = $this->toArray();
436                $output = [];
437                foreach ( $input as $item ) {
438                    $output[] = $item->toNative();
439                }
440
441                return $output;
442            case self::DNULL:
443            case self::DUNDEFINED:
444                return null;
445            default:
446                // @codeCoverageIgnoreStart
447                throw new RuntimeException( "Unknown type" );
448                // @codeCoverageIgnoreEnd
449        }
450    }
451
452    /**
453     * @return bool
454     */
455    public function toBool() {
456        return self::castTypes( $this, self::DBOOL )->data;
457    }
458
459    /**
460     * @return string
461     */
462    public function toString() {
463        return self::castTypes( $this, self::DSTRING )->data;
464    }
465
466    /**
467     * @return float
468     */
469    public function toFloat() {
470        return self::castTypes( $this, self::DFLOAT )->data;
471    }
472
473    /**
474     * @return int
475     */
476    public function toInt() {
477        return self::castTypes( $this, self::DINT )->data;
478    }
479
480    /**
481     * @return int|float
482     */
483    public function toNumber() {
484        // Types that can be cast to int
485        $intLikeTypes = [
486            self::DINT,
487            self::DBOOL,
488            self::DNULL
489        ];
490        return in_array( $this->type, $intLikeTypes, true ) ? $this->toInt() : $this->toFloat();
491    }
492
493    /**
494     * @return array
495     */
496    public function toArray() {
497        return self::castTypes( $this, self::DARRAY )->data;
498    }
499}