Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
17.65% |
18 / 102 |
|
5.00% |
1 / 20 |
CRAP | |
0.00% |
0 / 1 |
FormOptions | |
17.82% |
18 / 101 |
|
5.00% |
1 / 20 |
1796.39 | |
0.00% |
0 / 1 |
add | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
delete | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
guessType | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
validateName | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
setValue | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
getValue | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getValueReal | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
reset | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
consumeValue | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
consumeValues | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
validateIntBounds | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
validateBounds | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
42 | |||
getUnconsumedValues | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
30 | |||
getChangedValues | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getAllValues | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
fetchValuesFromRequest | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
182 | |||
offsetExists | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
offsetGet | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
offsetSet | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
offsetUnset | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Helper class to keep track of options when mixing links and form elements. |
4 | * |
5 | * Copyright © 2008, Niklas Laxström |
6 | * Copyright © 2011, Antoine Musso |
7 | * Copyright © 2013, Bartosz Dziewoński |
8 | * |
9 | * This program is free software; you can redistribute it and/or modify |
10 | * it under the terms of the GNU General Public License as published by |
11 | * the Free Software Foundation; either version 2 of the License, or |
12 | * (at your option) any later version. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License along |
20 | * with this program; if not, write to the Free Software Foundation, Inc., |
21 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
22 | * http://www.gnu.org/copyleft/gpl.html |
23 | * |
24 | * @file |
25 | * @author Niklas Laxström |
26 | * @author Antoine Musso |
27 | */ |
28 | |
29 | namespace MediaWiki\Html; |
30 | |
31 | use ArrayAccess; |
32 | use InvalidArgumentException; |
33 | use MediaWiki\Request\WebRequest; |
34 | use UnexpectedValueException; |
35 | |
36 | /** |
37 | * Helper class to keep track of options when mixing links and form elements. |
38 | * |
39 | * @todo This badly needs some examples and tests :) The usage in SpecialRecentchanges class is a |
40 | * good ersatz in the meantime. |
41 | */ |
42 | class FormOptions implements ArrayAccess { |
43 | /** @name Type constants |
44 | * Used internally to map an option value to a WebRequest accessor |
45 | * @{ |
46 | */ |
47 | /** Mark value for automatic detection (for simple data types only) */ |
48 | public const AUTO = -1; |
49 | /** String type, maps guessType() to WebRequest::getText() */ |
50 | public const STRING = 0; |
51 | /** Integer type, maps guessType() to WebRequest::getInt() */ |
52 | public const INT = 1; |
53 | /** Float type, maps guessType() to WebRequest::getFloat() |
54 | * @since 1.23 |
55 | */ |
56 | public const FLOAT = 4; |
57 | /** Boolean type, maps guessType() to WebRequest::getBool() */ |
58 | public const BOOL = 2; |
59 | /** Integer type or null, maps to WebRequest::getIntOrNull() |
60 | * This is useful for the namespace selector. |
61 | */ |
62 | public const INTNULL = 3; |
63 | /** Array type, maps guessType() to WebRequest::getArray() |
64 | * @since 1.29 |
65 | */ |
66 | public const ARR = 5; |
67 | /** @} */ |
68 | |
69 | /** |
70 | * Map of known option names to information about them. |
71 | * |
72 | * Each value is an array with the following keys: |
73 | * - 'default' - the default value as passed to add() |
74 | * - 'value' - current value, start with null, can be set by various functions |
75 | * - 'consumed' - true/false, whether the option was consumed using |
76 | * consumeValue() or consumeValues() |
77 | * - 'type' - one of the type constants (but never AUTO) |
78 | */ |
79 | protected $options = []; |
80 | |
81 | # Setting up |
82 | |
83 | /** |
84 | * Add an option to be handled by this FormOptions instance. |
85 | * |
86 | * @param string $name Request parameter name |
87 | * @param mixed $default Default value when the request parameter is not present |
88 | * @param int $type One of the type constants (optional, defaults to AUTO) |
89 | */ |
90 | public function add( $name, $default, $type = self::AUTO ) { |
91 | $option = []; |
92 | $option['default'] = $default; |
93 | $option['value'] = null; |
94 | $option['consumed'] = false; |
95 | |
96 | if ( $type !== self::AUTO ) { |
97 | $option['type'] = $type; |
98 | } else { |
99 | $option['type'] = self::guessType( $default ); |
100 | } |
101 | |
102 | $this->options[$name] = $option; |
103 | } |
104 | |
105 | /** |
106 | * Remove an option being handled by this FormOptions instance. This is the inverse of add(). |
107 | * |
108 | * @param string $name Request parameter name |
109 | */ |
110 | public function delete( $name ) { |
111 | $this->validateName( $name, true ); |
112 | unset( $this->options[$name] ); |
113 | } |
114 | |
115 | /** |
116 | * Used to find out which type the data is. All types are defined in the 'Type constants' section |
117 | * of this class. |
118 | * |
119 | * Detection of the INTNULL type is not supported; INT will be assumed if the data is an integer. |
120 | * |
121 | * @param mixed $data Value to guess the type for |
122 | * @return int Type constant |
123 | */ |
124 | public static function guessType( $data ) { |
125 | if ( is_bool( $data ) ) { |
126 | return self::BOOL; |
127 | } elseif ( is_int( $data ) ) { |
128 | return self::INT; |
129 | } elseif ( is_float( $data ) ) { |
130 | return self::FLOAT; |
131 | } elseif ( is_string( $data ) ) { |
132 | return self::STRING; |
133 | } elseif ( is_array( $data ) ) { |
134 | return self::ARR; |
135 | } else { |
136 | throw new InvalidArgumentException( 'Unsupported datatype' ); |
137 | } |
138 | } |
139 | |
140 | # Handling values |
141 | |
142 | /** |
143 | * Verify that the given option name exists. |
144 | * |
145 | * @param string $name Option name |
146 | * @param bool $strict Throw an exception when the option doesn't exist instead of returning false |
147 | * @return bool True if the option exists, false otherwise |
148 | */ |
149 | public function validateName( $name, $strict = false ) { |
150 | if ( !isset( $this->options[$name] ) ) { |
151 | if ( $strict ) { |
152 | throw new InvalidArgumentException( "Invalid option $name" ); |
153 | } else { |
154 | return false; |
155 | } |
156 | } |
157 | |
158 | return true; |
159 | } |
160 | |
161 | /** |
162 | * Use to set the value of an option. |
163 | * |
164 | * @param string $name Option name |
165 | * @param mixed $value Value for the option |
166 | * @param bool $force Whether to set the value when it is equivalent to the default value for this |
167 | * option (default false). |
168 | */ |
169 | public function setValue( $name, $value, $force = false ) { |
170 | $this->validateName( $name, true ); |
171 | |
172 | if ( !$force && $value === $this->options[$name]['default'] ) { |
173 | // null default values as unchanged |
174 | $this->options[$name]['value'] = null; |
175 | } else { |
176 | $this->options[$name]['value'] = $value; |
177 | } |
178 | } |
179 | |
180 | /** |
181 | * Get the value for the given option name. Uses getValueReal() internally. |
182 | * |
183 | * @param string $name Option name |
184 | * @return mixed |
185 | * @return-taint tainted This actually depends on the type of the option, but there's no way to determine that |
186 | * statically. |
187 | */ |
188 | public function getValue( $name ) { |
189 | $this->validateName( $name, true ); |
190 | |
191 | return $this->getValueReal( $this->options[$name] ); |
192 | } |
193 | |
194 | /** |
195 | * Return current option value, based on a structure taken from $options. |
196 | * |
197 | * @param array $option Array structure describing the option |
198 | * @return mixed Value, or the default value if it is null |
199 | */ |
200 | protected function getValueReal( $option ) { |
201 | if ( $option['value'] !== null ) { |
202 | return $option['value']; |
203 | } else { |
204 | return $option['default']; |
205 | } |
206 | } |
207 | |
208 | /** |
209 | * Delete the option value. |
210 | * This will make future calls to getValue() return the default value. |
211 | * @param string $name Option name |
212 | */ |
213 | public function reset( $name ) { |
214 | $this->validateName( $name, true ); |
215 | $this->options[$name]['value'] = null; |
216 | } |
217 | |
218 | /** |
219 | * Get the value of given option and mark it as 'consumed'. Consumed options are not returned |
220 | * by getUnconsumedValues(). Callers should verify that the given option exists. |
221 | * |
222 | * @see consumeValues() |
223 | * @param string $name Option name |
224 | * @return mixed Value, or the default value if it is null |
225 | */ |
226 | public function consumeValue( $name ) { |
227 | $this->validateName( $name, true ); |
228 | $this->options[$name]['consumed'] = true; |
229 | |
230 | return $this->getValueReal( $this->options[$name] ); |
231 | } |
232 | |
233 | /** |
234 | * Get the values of given options and mark them as 'consumed'. Consumed options are not returned |
235 | * by getUnconsumedValues(). Callers should verify that all the given options exist. |
236 | * |
237 | * @see consumeValue() |
238 | * @param string[] $names List of option names |
239 | * @return array Array of option values, or the default values if they are null |
240 | */ |
241 | public function consumeValues( $names ) { |
242 | $out = []; |
243 | |
244 | foreach ( $names as $name ) { |
245 | $this->validateName( $name, true ); |
246 | $this->options[$name]['consumed'] = true; |
247 | $out[] = $this->getValueReal( $this->options[$name] ); |
248 | } |
249 | |
250 | return $out; |
251 | } |
252 | |
253 | /** |
254 | * @see validateBounds() |
255 | * @param string $name |
256 | * @param int $min |
257 | * @param int $max |
258 | */ |
259 | public function validateIntBounds( $name, $min, $max ) { |
260 | $this->validateBounds( $name, $min, $max ); |
261 | } |
262 | |
263 | /** |
264 | * Constrain a numeric value for a given option to a given range. The value will be altered to fit |
265 | * in the range. |
266 | * |
267 | * @since 1.23 |
268 | * |
269 | * @param string $name Option name. Must be of numeric type. |
270 | * @param int|float $min Minimum value |
271 | * @param int|float $max Maximum value |
272 | */ |
273 | public function validateBounds( $name, $min, $max ) { |
274 | $this->validateName( $name, true ); |
275 | $type = $this->options[$name]['type']; |
276 | |
277 | if ( $type !== self::INT && $type !== self::INTNULL && $type !== self::FLOAT ) { |
278 | throw new InvalidArgumentException( "Type of option $name is not numeric" ); |
279 | } |
280 | |
281 | $value = $this->getValueReal( $this->options[$name] ); |
282 | if ( $type !== self::INTNULL || $value !== null ) { |
283 | $this->setValue( $name, max( $min, min( $max, $value ) ) ); |
284 | } |
285 | } |
286 | |
287 | /** |
288 | * Get all remaining values which have not been consumed by consumeValue() or consumeValues(). |
289 | * |
290 | * @param bool $all Whether to include unchanged options (default: false) |
291 | * @return array |
292 | */ |
293 | public function getUnconsumedValues( $all = false ) { |
294 | $values = []; |
295 | |
296 | foreach ( $this->options as $name => $data ) { |
297 | if ( !$data['consumed'] ) { |
298 | if ( $all || $data['value'] !== null ) { |
299 | $values[$name] = $this->getValueReal( $data ); |
300 | } |
301 | } |
302 | } |
303 | |
304 | return $values; |
305 | } |
306 | |
307 | /** |
308 | * Return options modified as an array ( name => value ) |
309 | * @return array |
310 | */ |
311 | public function getChangedValues() { |
312 | $values = []; |
313 | |
314 | foreach ( $this->options as $name => $data ) { |
315 | if ( $data['value'] !== null ) { |
316 | $values[$name] = $data['value']; |
317 | } |
318 | } |
319 | |
320 | return $values; |
321 | } |
322 | |
323 | /** |
324 | * Format options to an array ( name => value ) |
325 | * @return array |
326 | */ |
327 | public function getAllValues() { |
328 | $values = []; |
329 | |
330 | foreach ( $this->options as $name => $data ) { |
331 | $values[$name] = $this->getValueReal( $data ); |
332 | } |
333 | |
334 | return $values; |
335 | } |
336 | |
337 | # Reading values |
338 | |
339 | /** |
340 | * Fetch values for all options (or selected options) from the given WebRequest, making them |
341 | * available for accessing with getValue() or consumeValue() etc. |
342 | * |
343 | * @param WebRequest $r The request to fetch values from |
344 | * @param array|null $optionKeys Which options to fetch the values for (default: |
345 | * all of them). Note that passing an empty array will also result in |
346 | * values for all keys being fetched. |
347 | */ |
348 | public function fetchValuesFromRequest( WebRequest $r, $optionKeys = null ) { |
349 | if ( !$optionKeys ) { |
350 | $optionKeys = array_keys( $this->options ); |
351 | } |
352 | |
353 | foreach ( $optionKeys as $name ) { |
354 | $default = $this->options[$name]['default']; |
355 | $type = $this->options[$name]['type']; |
356 | |
357 | switch ( $type ) { |
358 | case self::BOOL: |
359 | $value = $r->getBool( $name, $default ); |
360 | break; |
361 | case self::INT: |
362 | $value = $r->getInt( $name, $default ); |
363 | break; |
364 | case self::FLOAT: |
365 | $value = $r->getFloat( $name, $default ); |
366 | break; |
367 | case self::STRING: |
368 | $value = $r->getText( $name, $default ); |
369 | break; |
370 | case self::INTNULL: |
371 | $value = $r->getIntOrNull( $name ); |
372 | break; |
373 | case self::ARR: |
374 | $value = $r->getArray( $name ); |
375 | |
376 | if ( $value !== null ) { |
377 | // Reject nested arrays (T344931) |
378 | $value = array_filter( $value, 'is_scalar' ); |
379 | } |
380 | break; |
381 | default: |
382 | throw new UnexpectedValueException( "Unsupported datatype $type" ); |
383 | } |
384 | |
385 | if ( $value !== null ) { |
386 | $this->options[$name]['value'] = $value === $default ? null : $value; |
387 | } |
388 | } |
389 | } |
390 | |
391 | /***************************************************************************/ |
392 | // region ArrayAccess functions |
393 | /** @name ArrayAccess functions |
394 | * These functions implement the ArrayAccess PHP interface. |
395 | * @see https://www.php.net/manual/en/class.arrayaccess.php |
396 | */ |
397 | |
398 | /** |
399 | * Whether the option exists. |
400 | * @param string $name |
401 | * @return bool |
402 | */ |
403 | public function offsetExists( $name ): bool { |
404 | return isset( $this->options[$name] ); |
405 | } |
406 | |
407 | /** |
408 | * Retrieve an option value. |
409 | * @param string $name |
410 | * @return mixed |
411 | */ |
412 | #[\ReturnTypeWillChange] |
413 | public function offsetGet( $name ) { |
414 | return $this->getValue( $name ); |
415 | } |
416 | |
417 | /** |
418 | * Set an option to given value. |
419 | * @param string $name |
420 | * @param mixed $value |
421 | */ |
422 | public function offsetSet( $name, $value ): void { |
423 | $this->setValue( $name, $value ); |
424 | } |
425 | |
426 | /** |
427 | * Delete the option. |
428 | * @param string $name |
429 | */ |
430 | public function offsetUnset( $name ): void { |
431 | $this->delete( $name ); |
432 | } |
433 | |
434 | // endregion -- end of ArrayAccess functions |
435 | } |
436 | |
437 | /** @deprecated class alias since 1.40 */ |
438 | class_alias( FormOptions::class, 'FormOptions' ); |