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