Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
17.65% covered (danger)
17.65%
18 / 102
5.00% covered (danger)
5.00%
1 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
FormOptions
17.82% covered (danger)
17.82%
18 / 101
5.00% covered (danger)
5.00%
1 / 20
1796.39
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 / 28
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 * 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
29namespace MediaWiki\Html;
30
31use ArrayAccess;
32use InvalidArgumentException;
33use MediaWiki\Request\WebRequest;
34use 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 */
42class 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 */
438class_alias( FormOptions::class, 'FormOptions' );