Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.51% covered (success)
94.51%
396 / 419
75.00% covered (warning)
75.00%
30 / 40
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiResult
94.74% covered (success)
94.74%
396 / 418
75.00% covered (warning)
75.00%
30 / 40
218.55
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setErrorFormatter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 serializeForApiResult
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reset
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getResultData
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 getSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setValue
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
13
 validateValue
97.50% covered (success)
97.50%
39 / 40
0.00% covered (danger)
0.00%
0 / 1
19
 addValue
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 unsetValue
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 removeValue
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 setContentValue
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 addContentValue
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 addParsedLimit
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setContentField
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 addContentField
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setSubelementsList
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addSubelementsList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 unsetSubelementsList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 removeSubelementsList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setIndexedTagName
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 addIndexedTagName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setIndexedTagNameRecursive
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
5.12
 addIndexedTagNameRecursive
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setPreserveKeysList
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addPreserveKeysList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 unsetPreserveKeysList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 removePreserveKeysList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setArrayType
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 addArrayType
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setArrayTypeRecursive
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 addArrayTypeRecursive
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isMetadataKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 applyTransformations
96.48% covered (success)
96.48%
137 / 142
0.00% covered (danger)
0.00%
0 / 1
68
 stripMetadata
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
11
 stripMetadataNonRecursive
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
10
 size
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 path
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
8.06
 addMetadataToResultVars
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
10
 formatExpiry
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Api;
8
9use Exception;
10use InvalidArgumentException;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Message\Message;
13use RuntimeException;
14use stdClass;
15use UnexpectedValueException;
16use Wikimedia\Message\ListParam;
17use Wikimedia\Message\MessageValue;
18use Wikimedia\Message\ParamType;
19use Wikimedia\Message\ScalarParam;
20use Wikimedia\Timestamp\TimestampFormat as TS;
21
22/**
23 * This class represents the result of the API operations.
24 * It simply wraps a nested array structure, adding some functions to simplify
25 * array's modifications. As various modules execute, they add different pieces
26 * of information to this result, structuring it as it will be given to the client.
27 *
28 * Each subarray may either be a dictionary - key-value pairs with unique keys,
29 * or lists, where the items are added using $data[] = $value notation.
30 *
31 * @since 1.25 this is no longer a subclass of ApiBase
32 * @ingroup API
33 */
34class ApiResult implements ApiSerializable {
35
36    /**
37     * Override existing value in addValue(), setValue(), and similar functions
38     * @since 1.21
39     */
40    public const OVERRIDE = 1;
41
42    /**
43     * For addValue(), setValue() and similar functions, if the value does not
44     * exist, add it as the first element. In case the new value has no name
45     * (numerical index), all indexes will be renumbered.
46     * @since 1.21
47     */
48    public const ADD_ON_TOP = 2;
49
50    /**
51     * For addValue() and similar functions, do not check size while adding a value
52     * Don't use this unless you REALLY know what you're doing.
53     * Values added while the size checking was disabled will never be counted.
54     * Ignored for setValue() and similar functions.
55     * @since 1.24
56     */
57    public const NO_SIZE_CHECK = 4;
58
59    /**
60     * For addValue(), setValue() and similar functions, do not validate data.
61     * Also disables size checking. If you think you need to use this, you're
62     * probably wrong.
63     * @since 1.25
64     */
65    public const NO_VALIDATE = self::NO_SIZE_CHECK | 8;
66
67    /**
68     * For addValue(), setValue() and similar functions, do allow override
69     * of conflicting keys.
70     * @since 1.45 (also backported to 1.43.6, 1.44.3)
71     */
72    public const IGNORE_CONFLICT_KEYS = 16;
73
74    /**
75     * Key for the 'indexed tag name' metadata item. Value is string.
76     * @since 1.25
77     */
78    public const META_INDEXED_TAG_NAME = '_element';
79
80    /**
81     * Key for the 'subelements' metadata item. Value is string[].
82     * @since 1.25
83     */
84    public const META_SUBELEMENTS = '_subelements';
85
86    /**
87     * Key for the 'preserve keys' metadata item. Value is string[].
88     * @since 1.25
89     */
90    public const META_PRESERVE_KEYS = '_preservekeys';
91
92    /**
93     * Key for the 'content' metadata item. Value is string.
94     * @since 1.25
95     */
96    public const META_CONTENT = '_content';
97
98    /**
99     * Key for the 'type' metadata item. Value is one of the following strings:
100     *  - default: Like 'array' if all (non-metadata) keys are numeric with no
101     *    gaps, otherwise like 'assoc'.
102     *  - array: Keys are used for ordering, but are not output. In a format
103     *    like JSON, outputs as [].
104     *  - assoc: In a format like JSON, outputs as {}.
105     *  - kvp: For a format like XML where object keys have a restricted
106     *    character set, use an alternative output format. For example,
107     *    <container><item name="key">value</item></container> rather than
108     *    <container key="value" />
109     *  - BCarray: Like 'array' normally, 'default' in backwards-compatibility mode.
110     *  - BCassoc: Like 'assoc' normally, 'default' in backwards-compatibility mode.
111     *  - BCkvp: Like 'kvp' normally. In backwards-compatibility mode, forces
112     *    the alternative output format for all formats, for example
113     *    [{"name":key,"*":value}] in JSON. META_KVP_KEY_NAME must also be set.
114     * @since 1.25
115     */
116    public const META_TYPE = '_type';
117
118    /**
119     * Key for the metadata item whose value specifies the name used for the
120     * kvp key in the alternative output format with META_TYPE 'kvp' or
121     * 'BCkvp', i.e. the "name" in <container><item name="key">value</item></container>.
122     * Value is string.
123     * @since 1.25
124     */
125    public const META_KVP_KEY_NAME = '_kvpkeyname';
126
127    /**
128     * Key for the metadata item that indicates that the KVP key should be
129     * added into an assoc value, i.e. {"key":{"val1":"a","val2":"b"}}
130     * transforms to {"name":"key","val1":"a","val2":"b"} rather than
131     * {"name":"key","value":{"val1":"a","val2":"b"}}.
132     * Value is boolean.
133     * @since 1.26
134     */
135    public const META_KVP_MERGE = '_kvpmerge';
136
137    /**
138     * Key for the 'BC bools' metadata item. Value is string[].
139     * Note no setter is provided.
140     * @since 1.25
141     */
142    public const META_BC_BOOLS = '_BC_bools';
143
144    /**
145     * Key for the 'BC subelements' metadata item. Value is string[].
146     * Note no setter is provided.
147     * @since 1.25
148     */
149    public const META_BC_SUBELEMENTS = '_BC_subelements';
150
151    /** @var mixed */
152    private $data;
153    private int $size;
154    /** @var int|false */
155    private $maxSize;
156    private ApiErrorFormatter $errorFormatter;
157
158    /**
159     * @param int|false $maxSize Maximum result "size", or false for no limit
160     */
161    public function __construct( $maxSize ) {
162        $this->maxSize = $maxSize;
163        $this->reset();
164    }
165
166    /**
167     * @since 1.25
168     * @param ApiErrorFormatter $formatter
169     */
170    public function setErrorFormatter( ApiErrorFormatter $formatter ) {
171        $this->errorFormatter = $formatter;
172    }
173
174    /**
175     * Allow for adding one ApiResult into another
176     * @since 1.25
177     * @return mixed
178     */
179    public function serializeForApiResult() {
180        return $this->data;
181    }
182
183    /***************************************************************************/
184    // region   Content
185    /** @name   Content */
186
187    /**
188     * Clear the current result data.
189     */
190    public function reset() {
191        $this->data = [
192            self::META_TYPE => 'assoc', // Usually what's desired
193        ];
194        $this->size = 0;
195    }
196
197    /**
198     * Get the result data array
199     *
200     * The returned value should be considered read-only.
201     *
202     * Transformations include:
203     *
204     * Custom: (callable) Applied before other transformations. Signature is
205     *  function ( &$data, &$metadata ), return value is ignored. Called for
206     *  each nested array.
207     *
208     * BC: (array) This transformation does various adjustments to bring the
209     *  output in line with the pre-1.25 result format. The value array is a
210     *  list of flags: 'nobool', 'no*', 'nosub'.
211     *  - Boolean-valued items are changed to '' if true or removed if false,
212     *    unless listed in META_BC_BOOLS. This may be skipped by including
213     *    'nobool' in the value array.
214     *  - The tag named by META_CONTENT is renamed to '*', and META_CONTENT is
215     *    set to '*'. This may be skipped by including 'no*' in the value
216     *    array.
217     *  - Tags listed in META_BC_SUBELEMENTS will have their values changed to
218     *    [ '*' => $value ]. This may be skipped by including 'nosub' in
219     *    the value array.
220     *  - If META_TYPE is 'BCarray', set it to 'default'
221     *  - If META_TYPE is 'BCassoc', set it to 'default'
222     *  - If META_TYPE is 'BCkvp', perform the transformation (even if
223     *    the Types transformation is not being applied).
224     *
225     * Types: (assoc) Apply transformations based on META_TYPE. The values
226     * array is an associative array with the following possible keys:
227     *  - AssocAsObject: (bool) If true, return arrays with META_TYPE 'assoc'
228     *    as objects.
229     *  - ArmorKVP: (string) If provided, transform arrays with META_TYPE 'kvp'
230     *    and 'BCkvp' into arrays of two-element arrays, something like this:
231     *      $output = [];
232     *      foreach ( $input as $key => $value ) {
233     *          $pair = [];
234     *          $pair[$META_KVP_KEY_NAME ?: $ArmorKVP_value] = $key;
235     *          ApiResult::setContentValue( $pair, 'value', $value );
236     *          $output[] = $pair;
237     *      }
238     *
239     * Strip: (string) Strips metadata keys from the result.
240     *  - 'all': Strip all metadata, recursively
241     *  - 'base': Strip metadata at the top-level only.
242     *  - 'none': Do not strip metadata.
243     *  - 'bc': Like 'all', but leave certain pre-1.25 keys.
244     *
245     * @since 1.25
246     * @param array|string|null $path Path to fetch, see ApiResult::addValue
247     * @param array $transforms See above
248     * @return mixed Result data, or null if not found
249     */
250    public function getResultData( $path = [], $transforms = [] ) {
251        $path = (array)$path;
252        if ( !$path ) {
253            return self::applyTransformations( $this->data, $transforms );
254        }
255
256        $last = array_pop( $path );
257        $ret = &$this->path( $path, 'dummy' );
258        if ( !isset( $ret[$last] ) ) {
259            return null;
260        } elseif ( is_array( $ret[$last] ) ) {
261            return self::applyTransformations( $ret[$last], $transforms );
262        } else {
263            return $ret[$last];
264        }
265    }
266
267    /**
268     * Get the size of the result, i.e. the amount of bytes in it
269     * @return int
270     */
271    public function getSize() {
272        return $this->size;
273    }
274
275    /**
276     * Add an output value to the array by name.
277     *
278     * Verifies that value with the same name has not been added before.
279     *
280     * @since 1.25
281     * @param array &$arr To add $value to
282     * @param string|int|null $name Index of $arr to add $value at,
283     *   or null to use the next numeric index.
284     * @param mixed $value
285     * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
286     */
287    public static function setValue( array &$arr, $name, $value, $flags = 0 ) {
288        if ( ( $flags & self::NO_VALIDATE ) !== self::NO_VALIDATE ) {
289            $value = self::validateValue( $value );
290        }
291
292        if ( $name === null ) {
293            if ( $flags & self::ADD_ON_TOP ) {
294                array_unshift( $arr, $value );
295            } else {
296                $arr[] = $value;
297            }
298            return;
299        }
300
301        $exists = isset( $arr[$name] );
302        if ( !$exists || ( $flags & self::OVERRIDE ) ) {
303            if ( !$exists && ( $flags & self::ADD_ON_TOP ) ) {
304                $arr = [ $name => $value ] + $arr;
305            } else {
306                $arr[$name] = $value;
307            }
308        } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
309            $conflicts = array_intersect_key( $arr[$name], $value );
310            if ( !$conflicts || ( $flags & self::IGNORE_CONFLICT_KEYS ) ) {
311                $arr[$name] += $value;
312            } else {
313                $keys = implode( ', ', array_keys( $conflicts ) );
314                throw new RuntimeException(
315                    "Conflicting keys ($keys) when attempting to merge element $name"
316                );
317            }
318        } elseif ( $value !== $arr[$name] ) {
319            throw new RuntimeException(
320                "Attempting to add element $name=$value, existing value is {$arr[$name]}"
321            );
322        }
323    }
324
325    /**
326     * Validate a value for addition to the result
327     * @param mixed $value
328     * @return array|mixed|string
329     */
330    private static function validateValue( $value ) {
331        if ( is_object( $value ) ) {
332            // Note we use is_callable() here instead of instanceof because
333            // ApiSerializable is an informal protocol (see docs there for details).
334            if ( is_callable( [ $value, 'serializeForApiResult' ] ) ) {
335                $oldValue = $value;
336                $value = $value->serializeForApiResult();
337                if ( is_object( $value ) ) {
338                    throw new UnexpectedValueException(
339                        get_class( $oldValue ) . '::serializeForApiResult() returned an object of class ' .
340                            get_class( $value )
341                    );
342                }
343
344                // Recursive call instead of fall-through so we can throw a
345                // better exception message.
346                try {
347                    return self::validateValue( $value );
348                } catch ( Exception $ex ) {
349                    throw new UnexpectedValueException(
350                        get_class( $oldValue ) . '::serializeForApiResult() returned an invalid value: ' .
351                            $ex->getMessage(),
352                        0,
353                        $ex
354                    );
355                }
356            } elseif ( $value instanceof ScalarParam || $value instanceof ListParam ) {
357                // HACK Support code that puts $msg->getParams() directly into API responses
358                // (e.g. ApiErrorFormatter::formatRawMessage()).
359                $value = $value->getType() === ParamType::TEXT ? $value->getValue() : $value->toJsonArray();
360                if ( $value instanceof MessageValue ) {
361                    $value = $value->toJsonArray();
362                }
363            } elseif ( $value instanceof MessageValue ) {
364                // HACK Support code that puts $msg->getParams() directly into API responses
365                // (e.g. ApiErrorFormatter::formatRawMessage()).
366                $value = $value->toJsonArray();
367            } elseif ( is_callable( [ $value, '__toString' ] ) ) {
368                $value = (string)$value;
369            } else {
370                $value = (array)$value + [ self::META_TYPE => 'assoc' ];
371            }
372        }
373
374        if ( is_string( $value ) ) {
375            // Optimization: avoid querying the service locator for each value.
376            static $contentLanguage = null;
377            if ( !$contentLanguage ) {
378                $contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
379            }
380            $value = $contentLanguage->normalize( $value );
381        } elseif ( is_array( $value ) ) {
382            foreach ( $value as $k => $v ) {
383                $value[$k] = self::validateValue( $v );
384            }
385        } elseif ( $value !== null && !is_scalar( $value ) ) {
386            $type = get_debug_type( $value );
387            throw new InvalidArgumentException( "Cannot add $type to ApiResult" );
388        } elseif ( is_float( $value ) && !is_finite( $value ) ) {
389            throw new InvalidArgumentException( 'Cannot add non-finite floats to ApiResult' );
390        }
391
392        return $value;
393    }
394
395    /**
396     * Add value to the output data at the given path.
397     *
398     * Path can be an indexed array, each element specifying the branch at which to add the new
399     * value. Setting $path to [ 'a', 'b', 'c' ] is equivalent to data['a']['b']['c'] = $value.
400     * If $path is null, the value will be inserted at the data root.
401     *
402     * @param array|string|int|null $path
403     * @param string|int|null $name See ApiResult::setValue()
404     * @param mixed $value
405     * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
406     *   This parameter used to be boolean, and the value of OVERRIDE=1 was specifically
407     *   chosen so that it would be backwards compatible with the new method signature.
408     * @return bool True if $value fits in the result, false if not
409     * @since 1.21 int $flags replaced boolean $override
410     */
411    public function addValue( $path, $name, $value, $flags = 0 ) {
412        $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
413
414        if ( !( $flags & self::NO_SIZE_CHECK ) ) {
415            // self::size needs the validated value. Then flag
416            // to not re-validate later.
417            $value = self::validateValue( $value );
418            $flags |= self::NO_VALIDATE;
419
420            $newsize = $this->size + self::size( $value );
421            if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
422                $this->errorFormatter->addWarning(
423                    'result', [ 'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ]
424                );
425                return false;
426            }
427            $this->size = $newsize;
428        }
429
430        self::setValue( $arr, $name, $value, $flags );
431        return true;
432    }
433
434    /**
435     * Remove an output value to the array by name.
436     * @param array &$arr To remove $value from
437     * @param string|int $name Index of $arr to remove
438     * @return mixed Old value, or null
439     */
440    public static function unsetValue( array &$arr, $name ) {
441        $ret = null;
442        if ( isset( $arr[$name] ) ) {
443            $ret = $arr[$name];
444            unset( $arr[$name] );
445        }
446        return $ret;
447    }
448
449    /**
450     * Remove value from the output data at the given path.
451     *
452     * @since 1.25
453     * @param array|string|null $path See ApiResult::addValue()
454     * @param string|int|null $name Index to remove at $path.
455     *   If null, $path itself is removed.
456     * @param int $flags Flags used when adding the value
457     * @return mixed Old value, or null
458     */
459    public function removeValue( $path, $name, $flags = 0 ) {
460        $path = (array)$path;
461        if ( $name === null ) {
462            if ( !$path ) {
463                throw new InvalidArgumentException( 'Cannot remove the data root' );
464            }
465            $name = array_pop( $path );
466        }
467        $ret = self::unsetValue( $this->path( $path, 'dummy' ), $name );
468        if ( !( $flags & self::NO_SIZE_CHECK ) ) {
469            $newsize = $this->size - self::size( $ret );
470            $this->size = max( $newsize, 0 );
471        }
472        return $ret;
473    }
474
475    /**
476     * Add an output value to the array by name and mark as META_CONTENT.
477     *
478     * @since 1.25
479     * @param array &$arr To add $value to
480     * @param string|int $name Index of $arr to add $value at.
481     * @param mixed $value
482     * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
483     */
484    public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) {
485        if ( $name === null ) {
486            throw new InvalidArgumentException( 'Content value must be named' );
487        }
488        self::setContentField( $arr, $name, $flags );
489        self::setValue( $arr, $name, $value, $flags );
490    }
491
492    /**
493     * Add value to the output data at the given path and mark as META_CONTENT
494     *
495     * @since 1.25
496     * @param array|string|null $path See ApiResult::addValue()
497     * @param string|int $name See ApiResult::setValue()
498     * @param mixed $value
499     * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
500     * @return bool True if $value fits in the result, false if not
501     */
502    public function addContentValue( $path, $name, $value, $flags = 0 ) {
503        if ( $name === null ) {
504            throw new InvalidArgumentException( 'Content value must be named' );
505        }
506        $this->addContentField( $path, $name, $flags );
507        return $this->addValue( $path, $name, $value, $flags );
508    }
509
510    /**
511     * Add the numeric limit for a limit=max to the result.
512     *
513     * @since 1.25
514     * @param string $moduleName
515     * @param int $limit
516     */
517    public function addParsedLimit( $moduleName, $limit ) {
518        // Add value, allowing overwriting
519        $this->addValue( 'limits', $moduleName, $limit,
520            self::OVERRIDE | self::NO_SIZE_CHECK );
521    }
522
523    // endregion -- end of Content
524
525    /***************************************************************************/
526    // region   Metadata
527    /** @name   Metadata */
528
529    /**
530     * Set the name of the content field name (META_CONTENT)
531     *
532     * @since 1.25
533     * @param array &$arr
534     * @param string|int $name Name of the field
535     * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
536     */
537    public static function setContentField( array &$arr, $name, $flags = 0 ) {
538        if ( isset( $arr[self::META_CONTENT] ) &&
539            isset( $arr[$arr[self::META_CONTENT]] ) &&
540            !( $flags & self::OVERRIDE )
541        ) {
542            throw new RuntimeException(
543                "Attempting to set content element as $name when " . $arr[self::META_CONTENT] .
544                    ' is already set as the content element'
545            );
546        }
547        $arr[self::META_CONTENT] = $name;
548    }
549
550    /**
551     * Set the name of the content field name (META_CONTENT)
552     *
553     * @since 1.25
554     * @param array|string|null $path See ApiResult::addValue()
555     * @param string|int $name Name of the field
556     * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
557     */
558    public function addContentField( $path, $name, $flags = 0 ) {
559        $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
560        self::setContentField( $arr, $name, $flags );
561    }
562
563    /**
564     * Causes the elements with the specified names to be output as
565     * subelements rather than attributes.
566     * @since 1.25 is static
567     * @param array &$arr
568     * @param array|string|int $names The element name(s) to be output as subelements
569     */
570    public static function setSubelementsList( array &$arr, $names ) {
571        if ( !isset( $arr[self::META_SUBELEMENTS] ) ) {
572            $arr[self::META_SUBELEMENTS] = (array)$names;
573        } else {
574            $arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names );
575        }
576    }
577
578    /**
579     * Causes the elements with the specified names to be output as
580     * subelements rather than attributes.
581     * @since 1.25
582     * @param array|string|null $path See ApiResult::addValue()
583     * @param array|string|int $names The element name(s) to be output as subelements
584     */
585    public function addSubelementsList( $path, $names ) {
586        $arr = &$this->path( $path );
587        self::setSubelementsList( $arr, $names );
588    }
589
590    /**
591     * Causes the elements with the specified names to be output as
592     * attributes (when possible) rather than as subelements.
593     * @since 1.25
594     * @param array &$arr
595     * @param array|string|int $names The element name(s) to not be output as subelements
596     */
597    public static function unsetSubelementsList( array &$arr, $names ) {
598        if ( isset( $arr[self::META_SUBELEMENTS] ) ) {
599            $arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names );
600        }
601    }
602
603    /**
604     * Causes the elements with the specified names to be output as
605     * attributes (when possible) rather than as subelements.
606     * @since 1.25
607     * @param array|string|null $path See ApiResult::addValue()
608     * @param array|string|int $names The element name(s) to not be output as subelements
609     */
610    public function removeSubelementsList( $path, $names ) {
611        $arr = &$this->path( $path );
612        self::unsetSubelementsList( $arr, $names );
613    }
614
615    /**
616     * Set the tag name for numeric-keyed values in XML format
617     * @since 1.25 is static
618     * @param array &$arr
619     * @param string $tag Tag name
620     */
621    public static function setIndexedTagName( array &$arr, $tag ) {
622        if ( !is_string( $tag ) ) {
623            throw new InvalidArgumentException( 'Bad tag name' );
624        }
625        $arr[self::META_INDEXED_TAG_NAME] = $tag;
626    }
627
628    /**
629     * Set the tag name for numeric-keyed values in XML format
630     * @since 1.25
631     * @param array|string|null $path See ApiResult::addValue()
632     * @param string $tag Tag name
633     */
634    public function addIndexedTagName( $path, $tag ) {
635        $arr = &$this->path( $path );
636        self::setIndexedTagName( $arr, $tag );
637    }
638
639    /**
640     * Set indexed tag name on $arr and all subarrays
641     *
642     * @since 1.25
643     * @param array &$arr
644     * @param string $tag Tag name
645     */
646    public static function setIndexedTagNameRecursive( array &$arr, $tag ) {
647        if ( !is_string( $tag ) ) {
648            throw new InvalidArgumentException( 'Bad tag name' );
649        }
650        $arr[self::META_INDEXED_TAG_NAME] = $tag;
651        foreach ( $arr as $k => &$v ) {
652            if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
653                self::setIndexedTagNameRecursive( $v, $tag );
654            }
655        }
656    }
657
658    /**
659     * Set indexed tag name on $path and all subarrays
660     *
661     * @since 1.25
662     * @param array|string|null $path See ApiResult::addValue()
663     * @param string $tag Tag name
664     */
665    public function addIndexedTagNameRecursive( $path, $tag ) {
666        $arr = &$this->path( $path );
667        self::setIndexedTagNameRecursive( $arr, $tag );
668    }
669
670    /**
671     * Preserve specified keys.
672     *
673     * This prevents XML name mangling and preventing keys from being removed
674     * by self::stripMetadata().
675     *
676     * @since 1.25
677     * @param array &$arr
678     * @param array|string $names The element name(s) to preserve
679     */
680    public static function setPreserveKeysList( array &$arr, $names ) {
681        if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) {
682            $arr[self::META_PRESERVE_KEYS] = (array)$names;
683        } else {
684            $arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names );
685        }
686    }
687
688    /**
689     * Preserve specified keys.
690     * @since 1.25
691     * @see self::setPreserveKeysList()
692     * @param array|string|null $path See ApiResult::addValue()
693     * @param array|string $names The element name(s) to preserve
694     */
695    public function addPreserveKeysList( $path, $names ) {
696        $arr = &$this->path( $path );
697        self::setPreserveKeysList( $arr, $names );
698    }
699
700    /**
701     * Don't preserve specified keys.
702     * @since 1.25
703     * @see self::setPreserveKeysList()
704     * @param array &$arr
705     * @param array|string $names The element name(s) to not preserve
706     */
707    public static function unsetPreserveKeysList( array &$arr, $names ) {
708        if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) {
709            $arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names );
710        }
711    }
712
713    /**
714     * Don't preserve specified keys.
715     * @since 1.25
716     * @see self::setPreserveKeysList()
717     * @param array|string|null $path See ApiResult::addValue()
718     * @param array|string $names The element name(s) to not preserve
719     */
720    public function removePreserveKeysList( $path, $names ) {
721        $arr = &$this->path( $path );
722        self::unsetPreserveKeysList( $arr, $names );
723    }
724
725    /**
726     * Set the array data type
727     *
728     * @since 1.25
729     * @param array &$arr
730     * @param string $type See ApiResult::META_TYPE
731     * @param string|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME
732     */
733    public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) {
734        if ( !in_array( $type, [
735                'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp'
736                ], true ) ) {
737            throw new InvalidArgumentException( 'Bad type' );
738        }
739        $arr[self::META_TYPE] = $type;
740        if ( is_string( $kvpKeyName ) ) {
741            $arr[self::META_KVP_KEY_NAME] = $kvpKeyName;
742        }
743    }
744
745    /**
746     * Set the array data type for a path
747     * @since 1.25
748     * @param array|string|null $path See ApiResult::addValue()
749     * @param string $tag See ApiResult::META_TYPE
750     * @param string|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME
751     */
752    public function addArrayType( $path, $tag, $kvpKeyName = null ) {
753        $arr = &$this->path( $path );
754        self::setArrayType( $arr, $tag, $kvpKeyName );
755    }
756
757    /**
758     * Set the array data type recursively
759     * @since 1.25
760     * @param array &$arr
761     * @param string $type See ApiResult::META_TYPE
762     * @param string|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME
763     */
764    public static function setArrayTypeRecursive( array &$arr, $type, $kvpKeyName = null ) {
765        self::setArrayType( $arr, $type, $kvpKeyName );
766        foreach ( $arr as $k => &$v ) {
767            if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
768                self::setArrayTypeRecursive( $v, $type, $kvpKeyName );
769            }
770        }
771    }
772
773    /**
774     * Set the array data type for a path recursively
775     * @since 1.25
776     * @param array|string|null $path See ApiResult::addValue()
777     * @param string $tag See ApiResult::META_TYPE
778     * @param string|null $kvpKeyName See ApiResult::META_KVP_KEY_NAME
779     */
780    public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) {
781        $arr = &$this->path( $path );
782        self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName );
783    }
784
785    // endregion -- end of Metadata
786
787    /***************************************************************************/
788    // region   Utility
789    /** @name   Utility */
790
791    /**
792     * Test whether a key should be considered metadata
793     *
794     * @param string|int $key
795     * @return bool
796     */
797    public static function isMetadataKey( $key ) {
798        return str_starts_with( $key, '_' );
799    }
800
801    /**
802     * Apply transformations to an array, returning the transformed array.
803     *
804     * @see ApiResult::getResultData()
805     * @since 1.25
806     * @param array $dataIn
807     * @param array $transforms
808     * @return array|stdClass
809     */
810    protected static function applyTransformations( array $dataIn, array $transforms ) {
811        $strip = $transforms['Strip'] ?? 'none';
812        if ( $strip === 'base' ) {
813            $transforms['Strip'] = 'none';
814        }
815        $transformTypes = $transforms['Types'] ?? null;
816        if ( $transformTypes !== null && !is_array( $transformTypes ) ) {
817            throw new InvalidArgumentException( __METHOD__ . ':Value for "Types" must be an array' );
818        }
819
820        $metadata = [];
821        $data = self::stripMetadataNonRecursive( $dataIn, $metadata );
822
823        if ( isset( $transforms['Custom'] ) ) {
824            if ( !is_callable( $transforms['Custom'] ) ) {
825                throw new InvalidArgumentException( __METHOD__ . ': Value for "Custom" must be callable' );
826            }
827            $transforms['Custom']( $data, $metadata );
828        }
829
830        if ( ( isset( $transforms['BC'] ) || $transformTypes !== null ) &&
831            isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] === 'BCkvp' &&
832            !isset( $metadata[self::META_KVP_KEY_NAME] )
833        ) {
834            throw new UnexpectedValueException( 'Type "BCkvp" used without setting ' .
835                'ApiResult::META_KVP_KEY_NAME metadata item' );
836        }
837
838        // BC transformations
839        $boolKeys = null;
840        if ( isset( $transforms['BC'] ) ) {
841            if ( !is_array( $transforms['BC'] ) ) {
842                throw new InvalidArgumentException( __METHOD__ . ':Value for "BC" must be an array' );
843            }
844            if ( !in_array( 'nobool', $transforms['BC'], true ) ) {
845                $boolKeys = isset( $metadata[self::META_BC_BOOLS] )
846                    ? array_fill_keys( $metadata[self::META_BC_BOOLS], true )
847                    : [];
848            }
849
850            if ( !in_array( 'no*', $transforms['BC'], true ) &&
851                isset( $metadata[self::META_CONTENT] ) && $metadata[self::META_CONTENT] !== '*'
852            ) {
853                $k = $metadata[self::META_CONTENT];
854                $data['*'] = $data[$k];
855                unset( $data[$k] );
856                $metadata[self::META_CONTENT] = '*';
857            }
858
859            if ( !in_array( 'nosub', $transforms['BC'], true ) &&
860                isset( $metadata[self::META_BC_SUBELEMENTS] )
861            ) {
862                foreach ( $metadata[self::META_BC_SUBELEMENTS] as $k ) {
863                    if ( isset( $data[$k] ) ) {
864                        $data[$k] = [
865                            '*' => $data[$k],
866                            self::META_CONTENT => '*',
867                            self::META_TYPE => 'assoc',
868                        ];
869                    }
870                }
871            }
872
873            if ( isset( $metadata[self::META_TYPE] ) ) {
874                switch ( $metadata[self::META_TYPE] ) {
875                    case 'BCarray':
876                    case 'BCassoc':
877                        $metadata[self::META_TYPE] = 'default';
878                        break;
879                    case 'BCkvp':
880                        $transformTypes['ArmorKVP'] = $metadata[self::META_KVP_KEY_NAME];
881                        break;
882                }
883            }
884        }
885
886        // Figure out type, do recursive calls, and do boolean transform if necessary
887        $defaultType = 'array';
888        $maxKey = -1;
889        foreach ( $data as $k => &$v ) {
890            $v = is_array( $v ) ? self::applyTransformations( $v, $transforms ) : $v;
891            if ( $boolKeys !== null && is_bool( $v ) && !isset( $boolKeys[$k] ) ) {
892                if ( !$v ) {
893                    unset( $data[$k] );
894                    continue;
895                }
896                $v = '';
897            }
898            if ( is_string( $k ) ) {
899                $defaultType = 'assoc';
900            } elseif ( $k > $maxKey ) {
901                $maxKey = $k;
902            }
903        }
904        unset( $v );
905
906        // Determine which metadata to keep
907        switch ( $strip ) {
908            case 'all':
909            case 'base':
910                $keepMetadata = [];
911                break;
912            case 'none':
913                $keepMetadata = &$metadata;
914                break;
915            case 'bc':
916                // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal Type mismatch on pass-by-ref args
917                $keepMetadata = array_intersect_key( $metadata, [
918                    self::META_INDEXED_TAG_NAME => 1,
919                    self::META_SUBELEMENTS => 1,
920                ] );
921                break;
922            default:
923                throw new InvalidArgumentException( __METHOD__ . ': Unknown value for "Strip"' );
924        }
925
926        // No type transformation
927        if ( $transformTypes === null ) {
928            return $data + $keepMetadata;
929        }
930
931        if ( $defaultType === 'array' && $maxKey !== count( $data ) - 1 ) {
932            $defaultType = 'assoc';
933        }
934
935        // Override type, if provided
936        $type = $defaultType;
937        if ( isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] !== 'default' ) {
938            $type = $metadata[self::META_TYPE];
939        }
940        if ( ( $type === 'kvp' || $type === 'BCkvp' ) &&
941            empty( $transformTypes['ArmorKVP'] )
942        ) {
943            $type = 'assoc';
944        } elseif ( $type === 'BCarray' ) {
945            $type = 'array';
946        } elseif ( $type === 'BCassoc' ) {
947            $type = 'assoc';
948        }
949
950        // Apply transformation
951        switch ( $type ) {
952            case 'assoc':
953                $metadata[self::META_TYPE] = 'assoc';
954                $data += $keepMetadata;
955                return empty( $transformTypes['AssocAsObject'] ) ? $data : (object)$data;
956
957            case 'array':
958                // Sort items in ascending order by key. Note that $data may contain a mix of number and string keys,
959                // for which the sorting behavior of krsort() with SORT_REGULAR is inconsistent between PHP versions.
960                // Given a comparison of a string key and a number key, PHP < 8.2 coerces the string key into a number
961                // (which yields zero if the string was non-numeric), and then performs the comparison,
962                // while PHP >= 8.2 makes the behavior consistent with stricter numeric comparisons introduced by
963                // PHP 8.0 in that if the string key is non-numeric, it converts the number key into a string
964                // and compares those two strings instead. We therefore use a custom comparison function
965                // implementing PHP >= 8.2 ordering semantics to ensure consistent ordering of items
966                // irrespective of the PHP version (T326480).
967                uksort( $data, static function ( $a, $b ): int {
968                    // In a comparison of a number or numeric string with a non-numeric string,
969                    // coerce both values into a string prior to comparing and compare the resulting strings.
970                    if ( is_numeric( $a ) xor is_numeric( $b ) ) {
971                        return (string)$a <=> (string)$b;
972                    }
973
974                    return $a <=> $b;
975                } );
976
977                $data = array_values( $data );
978                $metadata[self::META_TYPE] = 'array';
979                return $data + $keepMetadata;
980
981            case 'kvp':
982            case 'BCkvp':
983                $key = $metadata[self::META_KVP_KEY_NAME] ?? $transformTypes['ArmorKVP'];
984                $valKey = isset( $transforms['BC'] ) ? '*' : 'value';
985                $assocAsObject = !empty( $transformTypes['AssocAsObject'] );
986                $merge = !empty( $metadata[self::META_KVP_MERGE] );
987
988                $ret = [];
989                foreach ( $data as $k => $v ) {
990                    if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) {
991                        $vArr = (array)$v;
992                        if ( isset( $vArr[self::META_TYPE] ) ) {
993                            $mergeType = $vArr[self::META_TYPE];
994                        } elseif ( is_object( $v ) ) {
995                            $mergeType = 'assoc';
996                        } else {
997                            $keys = array_keys( $vArr );
998                            sort( $keys, SORT_NUMERIC );
999                            $mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc';
1000                        }
1001                    } else {
1002                        $mergeType = 'n/a';
1003                    }
1004                    if ( $mergeType === 'assoc' ) {
1005                        // @phan-suppress-next-line PhanPossiblyUndeclaredVariable vArr set when used
1006                        $item = $vArr + [
1007                            $key => $k,
1008                        ];
1009                        if ( $strip === 'none' ) {
1010                            self::setPreserveKeysList( $item, [ $key ] );
1011                        }
1012                    } else {
1013                        $item = [
1014                            $key => $k,
1015                            $valKey => $v,
1016                        ];
1017                        if ( $strip === 'none' ) {
1018                            $item += [
1019                                self::META_PRESERVE_KEYS => [ $key ],
1020                                self::META_CONTENT => $valKey,
1021                                self::META_TYPE => 'assoc',
1022                            ];
1023                        }
1024                    }
1025                    $ret[] = $assocAsObject ? (object)$item : $item;
1026                }
1027                $metadata[self::META_TYPE] = 'array';
1028
1029                return $ret + $keepMetadata;
1030
1031            default:
1032                throw new UnexpectedValueException( "Unknown type '$type'" );
1033        }
1034    }
1035
1036    /**
1037     * Recursively remove metadata keys from a data array or object
1038     *
1039     * Note this removes all potential metadata keys, not just the defined
1040     * ones.
1041     *
1042     * @since 1.25
1043     * @param array|stdClass $data
1044     * @return array|stdClass
1045     */
1046    public static function stripMetadata( $data ) {
1047        if ( is_array( $data ) || is_object( $data ) ) {
1048            $isObj = is_object( $data );
1049            if ( $isObj ) {
1050                $data = (array)$data;
1051            }
1052            $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1053                ? (array)$data[self::META_PRESERVE_KEYS]
1054                : [];
1055            foreach ( $data as $k => $v ) {
1056                if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1057                    unset( $data[$k] );
1058                } elseif ( is_array( $v ) || is_object( $v ) ) {
1059                    $data[$k] = self::stripMetadata( $v );
1060                }
1061            }
1062            if ( $isObj ) {
1063                $data = (object)$data;
1064            }
1065        }
1066        return $data;
1067    }
1068
1069    /**
1070     * Remove metadata keys from a data array or object, non-recursive
1071     *
1072     * Note this removes all potential metadata keys, not just the defined
1073     * ones.
1074     *
1075     * @since 1.25
1076     * @param array|stdClass $data
1077     * @param array|null &$metadata Store metadata here, if provided
1078     * @return array|stdClass
1079     */
1080    public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
1081        if ( !is_array( $metadata ) ) {
1082            $metadata = [];
1083        }
1084        if ( is_array( $data ) || is_object( $data ) ) {
1085            $isObj = is_object( $data );
1086            if ( $isObj ) {
1087                $data = (array)$data;
1088            }
1089            $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1090                ? (array)$data[self::META_PRESERVE_KEYS]
1091                : [];
1092            foreach ( $data as $k => $v ) {
1093                if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1094                    $metadata[$k] = $v;
1095                    unset( $data[$k] );
1096                }
1097            }
1098            if ( $isObj ) {
1099                $data = (object)$data;
1100            }
1101        }
1102        return $data;
1103    }
1104
1105    /**
1106     * Get the 'real' size of a result item. This means the strlen() of the item,
1107     * or the sum of the strlen()s of the elements if the item is an array.
1108     * @param mixed $value Validated value (see self::validateValue())
1109     * @return int
1110     */
1111    private static function size( $value ) {
1112        $s = 0;
1113        if ( is_array( $value ) ) {
1114            foreach ( $value as $k => $v ) {
1115                if ( !self::isMetadataKey( $k ) ) {
1116                    $s += self::size( $v );
1117                }
1118            }
1119        } elseif ( is_scalar( $value ) ) {
1120            $s = strlen( $value );
1121        }
1122
1123        return $s;
1124    }
1125
1126    /**
1127     * Return a reference to the internal data at $path
1128     *
1129     * @param array|string|null $path
1130     * @param string $create
1131     *   If 'append', append empty arrays.
1132     *   If 'prepend', prepend empty arrays.
1133     *   If 'dummy', return a dummy array.
1134     *   Else, raise an error.
1135     * @return array
1136     */
1137    private function &path( $path, $create = 'append' ) {
1138        $path = (array)$path;
1139        $ret = &$this->data;
1140        foreach ( $path as $i => $k ) {
1141            if ( !isset( $ret[$k] ) ) {
1142                switch ( $create ) {
1143                    case 'append':
1144                        $ret[$k] = [];
1145                        break;
1146                    case 'prepend':
1147                        $ret = [ $k => [] ] + $ret;
1148                        break;
1149                    case 'dummy':
1150                        $tmp = [];
1151                        return $tmp;
1152                    default:
1153                        $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1154                        throw new InvalidArgumentException( "Path $fail does not exist" );
1155                }
1156            }
1157            if ( !is_array( $ret[$k] ) ) {
1158                $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1159                throw new InvalidArgumentException( "Path $fail is not an array" );
1160            }
1161            $ret = &$ret[$k];
1162        }
1163        return $ret;
1164    }
1165
1166    /**
1167     * Add the correct metadata to an array of vars we want to export through
1168     * the API.
1169     *
1170     * @param array $vars
1171     * @param bool $forceHash
1172     * @return array
1173     */
1174    public static function addMetadataToResultVars( $vars, $forceHash = true ) {
1175        // Process subarrays and determine if this is a JS [] or {}
1176        $hash = $forceHash;
1177        $maxKey = -1;
1178        $bools = [];
1179        foreach ( $vars as $k => $v ) {
1180            if ( is_array( $v ) || is_object( $v ) ) {
1181                $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) );
1182            } elseif ( is_bool( $v ) ) {
1183                // Better here to use real bools even in BC formats
1184                $bools[] = $k;
1185            }
1186            if ( is_string( $k ) ) {
1187                $hash = true;
1188            } elseif ( $k > $maxKey ) {
1189                $maxKey = $k;
1190            }
1191        }
1192        if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
1193            $hash = true;
1194        }
1195
1196        // Set metadata appropriately
1197        if ( $hash ) {
1198            // Get the list of keys we actually care about. Unfortunately, we can't support
1199            // certain keys that conflict with ApiResult metadata.
1200            $keys = array_diff( array_keys( $vars ), [
1201                self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME,
1202                self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS
1203            ] );
1204
1205            return [
1206                self::META_TYPE => 'kvp',
1207                self::META_KVP_KEY_NAME => 'key',
1208                self::META_PRESERVE_KEYS => $keys,
1209                self::META_BC_BOOLS => $bools,
1210                self::META_INDEXED_TAG_NAME => 'var',
1211            ] + $vars;
1212        } else {
1213            return [
1214                self::META_TYPE => 'array',
1215                self::META_BC_BOOLS => $bools,
1216                self::META_INDEXED_TAG_NAME => 'value',
1217            ] + $vars;
1218        }
1219    }
1220
1221    /**
1222     * Format an expiry timestamp for API output
1223     * @since 1.29
1224     * @param string|null|false $expiry Expiry timestamp, likely from the database
1225     * @param string $infinity Use this string for infinite expiry
1226     *  (only use this to maintain backward compatibility with existing output)
1227     * @return string Formatted expiry
1228     */
1229    public static function formatExpiry( $expiry, $infinity = 'infinity' ) {
1230        static $dbInfinity;
1231        $dbInfinity ??= MediaWikiServices::getInstance()->getConnectionProvider()
1232            ->getReplicaDatabase()
1233            ->getInfinity();
1234
1235        if ( $expiry === '' || $expiry === null || $expiry === false ||
1236            wfIsInfinity( $expiry ) || $expiry === $dbInfinity
1237        ) {
1238            return $infinity;
1239        } else {
1240            return wfTimestamp( TS::ISO_8601, $expiry );
1241        }
1242    }
1243
1244    // endregion -- end of Utility
1245
1246}
1247
1248/*
1249 * This file uses VisualStudio style region/endregion fold markers which are
1250 * recognised by PHPStorm. If modelines are enabled, the following editor
1251 * configuration will also enable folding in vim, if it is in the last 5 lines
1252 * of the file. We also use "@name" which creates sections in Doxygen.
1253 *
1254 * vim: foldmarker=//\ region,//\ endregion foldmethod=marker
1255 */
1256
1257/** @deprecated class alias since 1.43 */
1258class_alias( ApiResult::class, 'ApiResult' );