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 === '===' ) {