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