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