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.
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.
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
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.
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.
30 * @since 1.25 this is no longer a subclass of ApiBase
33 class ApiResult
implements ApiSerializable
{
36 * Override existing value in addValue(), setValue(), and similar functions
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.
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.
56 const NO_SIZE_CHECK
= 4;
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
64 const NO_VALIDATE
= 12;
67 * Key for the 'indexed tag name' metadata item. Value is string.
70 const META_INDEXED_TAG_NAME
= '_element';
73 * Key for the 'subelements' metadata item. Value is string[].
76 const META_SUBELEMENTS
= '_subelements';
79 * Key for the 'preserve keys' metadata item. Value is string[].
82 const META_PRESERVE_KEYS
= '_preservekeys';
85 * Key for the 'content' metadata item. Value is string.
88 const META_CONTENT
= '_content';
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.
108 const META_TYPE
= '_type';
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>.
117 const META_KVP_KEY_NAME
= '_kvpkeyname';
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"}}.
127 const META_KVP_MERGE
= '_kvpmerge';
130 * Key for the 'BC bools' metadata item. Value is string[].
131 * Note no setter is provided.
134 const META_BC_BOOLS
= '_BC_bools';
137 * Key for the 'BC subelements' metadata item. Value is string[].
138 * Note no setter is provided.
141 const META_BC_SUBELEMENTS
= '_BC_subelements';
143 private $data, $size, $maxSize;
144 private $errorFormatter;
147 private $checkingSize, $mainForContinuation;
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
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' );
161 $this->maxSize
= $maxSize;
162 $this->checkingSize
= true;
167 * Set the error formatter
169 * @param ApiErrorFormatter $formatter
171 public function setErrorFormatter( ApiErrorFormatter
$formatter ) {
172 $this->errorFormatter
= $formatter;
176 * Allow for adding one ApiResult into another
180 public function serializeForApiResult() {
184 /************************************************************************//**
190 * Clear the current result data.
192 public function reset() {
194 self
::META_TYPE
=> 'assoc', // Usually what's desired
200 * Get the result data array
202 * The returned value should be considered read-only.
204 * Transformations include:
206 * Custom: (callable) Applied before other transformations. Signature is
207 * function ( &$data, &$metadata ), return value is ignored. Called for
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
219 * - Tags listed in META_BC_SUBELEMENTS will have their values changed to
220 * [ '*' => $value ]. This may be skipped by including 'nosub' in
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).
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'
231 * - ArmorKVP: (string) If provided, transform arrays with META_TYPE 'kvp'
232 * and 'BCkvp' into arrays of two-element arrays, something like this:
234 * foreach ( $input as $key => $value ) {
236 * $pair[$META_KVP_KEY_NAME ?: $ArmorKVP_value] = $key;
237 * ApiResult::setContentValue( $pair, 'value', $value );
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.
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
252 public function getResultData( $path = [], $transforms = [] ) {
253 $path = (array)$path;
255 return self
::applyTransformations( $this->data
, $transforms );
258 $last = array_pop( $path );
259 $ret = &$this->path( $path, 'dummy' );
260 if ( !isset( $ret[$last] ) ) {
262 } elseif ( is_array( $ret[$last] ) ) {
263 return self
::applyTransformations( $ret[$last], $transforms );
270 * Get the size of the result, i.e. the amount of bytes in it
273 public function getSize() {
278 * Add an output value to the array by name.
280 * Verifies that value with the same name has not been added before.
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.
289 public static function setValue( array &$arr, $name, $value, $flags = 0 ) {
290 if ( ( $flags & ApiResult
::NO_VALIDATE
) !== ApiResult
::NO_VALIDATE
) {
291 $value = self
::validateValue( $value );
294 if ( $name === null ) {
295 if ( $flags & ApiResult
::ADD_ON_TOP
) {
296 array_unshift( $arr, $value );
298 array_push( $arr, $value );
303 $exists = isset( $arr[$name] );
304 if ( !$exists ||
( $flags & ApiResult
::OVERRIDE
) ) {
305 if ( !$exists && ( $flags & ApiResult
::ADD_ON_TOP
) ) {
306 $arr = [ $name => $value ] +
$arr;
308 $arr[$name] = $value;
310 } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
311 $conflicts = array_intersect_key( $arr[$name], $value );
313 $arr[$name] +
= $value;
315 $keys = implode( ', ', array_keys( $conflicts ) );
316 throw new RuntimeException(
317 "Conflicting keys ($keys) when attempting to merge element $name"
321 throw new RuntimeException(
322 "Attempting to add element $name=$value, existing value is {$arr[$name]}"
328 * Validate a value for addition to the result
329 * @param mixed $value
330 * @return array|mixed|string
332 private static function validateValue( $value ) {
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' ] ) ) {
340 $value = $value->serializeForApiResult();
341 if ( is_object( $value ) ) {
342 throw new UnexpectedValueException(
343 get_class( $oldValue ) . '::serializeForApiResult() returned an object of class ' .
348 // Recursive call instead of fall-through so we can throw a
349 // better exception message.
351 return self
::validateValue( $value );
352 } catch ( Exception
$ex ) {
353 throw new UnexpectedValueException(
354 get_class( $oldValue ) . '::serializeForApiResult() returned an invalid value: ' .
360 } elseif ( is_callable( [ $value, '__toString' ] ) ) {
361 $value = (string)$value;
363 $value = (array)$value +
[ self
::META_TYPE
=> 'assoc' ];
366 if ( is_array( $value ) ) {
367 // Work around PHP bug 45959 by copying to a temporary
368 // (in this case, foreach gets $k === "1" but $tmp[$k] assigns as if $k === 1)
370 foreach ( $value as $k => $v ) {
371 $tmp[$k] = self
::validateValue( $v );
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 ) . ')';
383 throw new InvalidArgumentException( "Cannot add $type to ApiResult" );
390 * Add value to the output data at the given path.
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.
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
405 public function addValue( $path, $name, $value, $flags = 0 ) {
406 $arr = &$this->path( $path, ( $flags & ApiResult
::ADD_ON_TOP
) ?
'prepend' : 'append' );
408 if ( $this->checkingSize
&& !( $flags & ApiResult
::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 |
= ApiResult
::NO_VALIDATE
;
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
) ]
421 $this->size
= $newsize;
424 self
::setValue( $arr, $name, $value, $flags );
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
434 public static function unsetValue( array &$arr, $name ) {
436 if ( isset( $arr[$name] ) ) {
438 unset( $arr[$name] );
444 * Remove value from the output data at the given path.
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
453 public function removeValue( $path, $name, $flags = 0 ) {
454 $path = (array)$path;
455 if ( $name === null ) {
457 throw new InvalidArgumentException( 'Cannot remove the data root' );
459 $name = array_pop( $path );
461 $ret = self
::unsetValue( $this->path( $path, 'dummy' ), $name );
462 if ( $this->checkingSize
&& !( $flags & ApiResult
::NO_SIZE_CHECK
) ) {
463 $newsize = $this->size
- self
::size( $ret );
464 $this->size
= max( $newsize, 0 );
470 * Add an output value to the array by name and mark as META_CONTENT.
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.
478 public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) {
479 if ( $name === null ) {
480 throw new InvalidArgumentException( 'Content value must be named' );
482 self
::setContentField( $arr, $name, $flags );
483 self
::setValue( $arr, $name, $value, $flags );
487 * Add value to the output data at the given path and mark as META_CONTENT
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
496 public function addContentValue( $path, $name, $value, $flags = 0 ) {
497 if ( $name === null ) {
498 throw new InvalidArgumentException( 'Content value must be named' );
500 $this->addContentField( $path, $name, $flags );
501 $this->addValue( $path, $name, $value, $flags );
505 * Add the numeric limit for a limit=max to the result.
508 * @param string $moduleName
511 public function addParsedLimit( $moduleName, $limit ) {
512 // Add value, allowing overwriting
513 $this->addValue( 'limits', $moduleName, $limit,
514 ApiResult
::OVERRIDE | ApiResult
::NO_SIZE_CHECK
);
519 /************************************************************************//**
525 * Set the name of the content field name (META_CONTENT)
529 * @param string|int $name Name of the field
530 * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP.
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
)
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'
542 $arr[self
::META_CONTENT
] = $name;
546 * Set the name of the content field name (META_CONTENT)
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.
553 public function addContentField( $path, $name, $flags = 0 ) {
554 $arr = &$this->path( $path, ( $flags & ApiResult
::ADD_ON_TOP
) ?
'prepend' : 'append' );
555 self
::setContentField( $arr, $name, $flags );
559 * Causes the elements with the specified names to be output as
560 * subelements rather than attributes.
561 * @since 1.25 is static
563 * @param array|string|int $names The element name(s) to be output as subelements
565 public static function setSubelementsList( array &$arr, $names ) {
566 if ( !isset( $arr[self
::META_SUBELEMENTS
] ) ) {
567 $arr[self
::META_SUBELEMENTS
] = (array)$names;
569 $arr[self
::META_SUBELEMENTS
] = array_merge( $arr[self
::META_SUBELEMENTS
], (array)$names );
574 * Causes the elements with the specified names to be output as
575 * subelements rather than attributes.
577 * @param array|string|null $path See ApiResult::addValue()
578 * @param array|string|int $names The element name(s) to be output as subelements
580 public function addSubelementsList( $path, $names ) {
581 $arr = &$this->path( $path );
582 self
::setSubelementsList( $arr, $names );
586 * Causes the elements with the specified names to be output as
587 * attributes (when possible) rather than as subelements.
590 * @param array|string|int $names The element name(s) to not be output as subelements
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 );
599 * Causes the elements with the specified names to be output as
600 * attributes (when possible) rather than as subelements.
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
605 public function removeSubelementsList( $path, $names ) {
606 $arr = &$this->path( $path );
607 self
::unsetSubelementsList( $arr, $names );
611 * Set the tag name for numeric-keyed values in XML format
612 * @since 1.25 is static
614 * @param string $tag Tag name
616 public static function setIndexedTagName( array &$arr, $tag ) {
617 if ( !is_string( $tag ) ) {
618 throw new InvalidArgumentException( 'Bad tag name' );
620 $arr[self
::META_INDEXED_TAG_NAME
] = $tag;
624 * Set the tag name for numeric-keyed values in XML format
626 * @param array|string|null $path See ApiResult::addValue()
627 * @param string $tag Tag name
629 public function addIndexedTagName( $path, $tag ) {
630 $arr = &$this->path( $path );
631 self
::setIndexedTagName( $arr, $tag );
635 * Set indexed tag name on $arr and all subarrays
639 * @param string $tag Tag name
641 public static function setIndexedTagNameRecursive( array &$arr, $tag ) {
642 if ( !is_string( $tag ) ) {
643 throw new InvalidArgumentException( 'Bad tag name' );
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 );
654 * Set indexed tag name on $path and all subarrays
657 * @param array|string|null $path See ApiResult::addValue()
658 * @param string $tag Tag name
660 public function addIndexedTagNameRecursive( $path, $tag ) {
661 $arr = &$this->path( $path );
662 self
::setIndexedTagNameRecursive( $arr, $tag );
666 * Preserve specified keys.
668 * This prevents XML name mangling and preventing keys from being removed
669 * by self::stripMetadata().
673 * @param array|string $names The element name(s) to preserve
675 public static function setPreserveKeysList( array &$arr, $names ) {
676 if ( !isset( $arr[self
::META_PRESERVE_KEYS
] ) ) {
677 $arr[self
::META_PRESERVE_KEYS
] = (array)$names;
679 $arr[self
::META_PRESERVE_KEYS
] = array_merge( $arr[self
::META_PRESERVE_KEYS
], (array)$names );
684 * Preserve specified keys.
686 * @see self::setPreserveKeysList()
687 * @param array|string|null $path See ApiResult::addValue()
688 * @param array|string $names The element name(s) to preserve
690 public function addPreserveKeysList( $path, $names ) {
691 $arr = &$this->path( $path );
692 self
::setPreserveKeysList( $arr, $names );
696 * Don't preserve specified keys.
698 * @see self::setPreserveKeysList()
700 * @param array|string $names The element name(s) to not preserve
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 );
709 * Don't preserve specified keys.
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
715 public function removePreserveKeysList( $path, $names ) {
716 $arr = &$this->path( $path );
717 self
::unsetPreserveKeysList( $arr, $names );
721 * Set the array data type
725 * @param string $type See ApiResult::META_TYPE
726 * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME
728 public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) {
729 if ( !in_array( $type, [
730 'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp'
732 throw new InvalidArgumentException( 'Bad type' );
734 $arr[self
::META_TYPE
] = $type;
735 if ( is_string( $kvpKeyName ) ) {
736 $arr[self
::META_KVP_KEY_NAME
] = $kvpKeyName;
741 * Set the array data type for a path
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
747 public function addArrayType( $path, $tag, $kvpKeyName = null ) {
748 $arr = &$this->path( $path );
749 self
::setArrayType( $arr, $tag, $kvpKeyName );
753 * Set the array data type recursively
756 * @param string $type See ApiResult::META_TYPE
757 * @param string $kvpKeyName See ApiResult::META_KVP_KEY_NAME
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 );
769 * Set the array data type for a path recursively
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
775 public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) {
776 $arr = &$this->path( $path );
777 self
::setArrayTypeRecursive( $arr, $tag, $kvpKeyName );
782 /************************************************************************//**
788 * Test whether a key should be considered metadata
793 public static function isMetadataKey( $key ) {
794 return substr( $key, 0, 1 ) === '_';
798 * Apply transformations to an array, returning the transformed array.
800 * @see ApiResult::getResultData()
802 * @param array $dataIn
803 * @param array $transforms
804 * @return array|object
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';
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' );
817 $data = self
::stripMetadataNonRecursive( $dataIn, $metadata );
819 if ( isset( $transforms['Custom'] ) ) {
820 if ( !is_callable( $transforms['Custom'] ) ) {
821 throw new InvalidArgumentException( __METHOD__
. ': Value for "Custom" must be callable' );
823 call_user_func_array( $transforms['Custom'], [ &$data, &$metadata ] );
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
] )
830 throw new UnexpectedValueException( 'Type "BCkvp" used without setting ' .
831 'ApiResult::META_KVP_KEY_NAME metadata item' );
834 // BC transformations
836 if ( isset( $transforms['BC'] ) ) {
837 if ( !is_array( $transforms['BC'] ) ) {
838 throw new InvalidArgumentException( __METHOD__
. ':Value for "BC" must be an array' );
840 if ( !in_array( 'nobool', $transforms['BC'], true ) ) {
841 $boolKeys = isset( $metadata[self
::META_BC_BOOLS
] )
842 ?
array_flip( $metadata[self
::META_BC_BOOLS
] )
846 if ( !in_array( 'no*', $transforms['BC'], true ) &&
847 isset( $metadata[self
::META_CONTENT
] ) && $metadata[self
::META_CONTENT
] !== '*'
849 $k = $metadata[self
::META_CONTENT
];
850 $data['*'] = $data[$k];
852 $metadata[self
::META_CONTENT
] = '*';
855 if ( !in_array( 'nosub', $transforms['BC'], true ) &&
856 isset( $metadata[self
::META_BC_SUBELEMENTS
] )
858 foreach ( $metadata[self
::META_BC_SUBELEMENTS
] as $k ) {
859 if ( isset( $data[$k] ) ) {
862 self
::META_CONTENT
=> '*',
863 self
::META_TYPE
=> 'assoc',
869 if ( isset( $metadata[self
::META_TYPE
] ) ) {
870 switch ( $metadata[self
::META_TYPE
] ) {
873 $metadata[self
::META_TYPE
] = 'default';
876 $transformTypes['ArmorKVP'] = $metadata[self
::META_KVP_KEY_NAME
];
882 // Figure out type, do recursive calls, and do boolean transform if necessary
883 $defaultType = 'array';
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] ) ) {
894 if ( is_string( $k ) ) {
895 $defaultType = 'assoc';
896 } elseif ( $k > $maxKey ) {
902 // Determine which metadata to keep
909 $keepMetadata = &$metadata;
912 $keepMetadata = array_intersect_key( $metadata, [
913 self
::META_INDEXED_TAG_NAME
=> 1,
914 self
::META_SUBELEMENTS
=> 1,
918 throw new InvalidArgumentException( __METHOD__
. ': Unknown value for "Strip"' );
921 // Type transformation
922 if ( $transformTypes !== null ) {
923 if ( $defaultType === 'array' && $maxKey !== count( $data ) - 1 ) {
924 $defaultType = 'assoc';
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
];
932 if ( ( $type === 'kvp' ||
$type === 'BCkvp' ) &&
933 empty( $transformTypes['ArmorKVP'] )
936 } elseif ( $type === 'BCarray' ) {
938 } elseif ( $type === 'BCassoc' ) {
942 // Apply transformation
945 $metadata[self
::META_TYPE
] = 'assoc';
946 $data +
= $keepMetadata;
947 return empty( $transformTypes['AssocAsObject'] ) ?
$data : (object)$data;
951 $data = array_values( $data );
952 $metadata[self
::META_TYPE
] = 'array';
953 return $data +
$keepMetadata;
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
] );
965 foreach ( $data as $k => $v ) {
966 if ( $merge && ( is_array( $v ) ||
is_object( $v ) ) ) {
968 if ( isset( $vArr[self
::META_TYPE
] ) ) {
969 $mergeType = $vArr[self
::META_TYPE
];
970 } elseif ( is_object( $v ) ) {
971 $mergeType = 'assoc';
973 $keys = array_keys( $vArr );
974 sort( $keys, SORT_NUMERIC
);
975 $mergeType = ( $keys === array_keys( $keys ) ) ?
'array' : 'assoc';
980 if ( $mergeType === 'assoc' ) {
984 if ( $strip === 'none' ) {
985 self
::setPreserveKeysList( $item, [ $key ] );
992 if ( $strip === 'none' ) {
994 self
::META_PRESERVE_KEYS
=> [ $key ],
995 self
::META_CONTENT
=> $valKey,
996 self
::META_TYPE
=> 'assoc',
1000 $ret[] = $assocAsObject ?
(object)$item : $item;
1002 $metadata[self
::META_TYPE
] = 'array';
1004 return $ret +
$keepMetadata;
1007 throw new UnexpectedValueException( "Unknown type '$type'" );
1010 return $data +
$keepMetadata;
1015 * Recursively remove metadata keys from a data array or object
1017 * Note this removes all potential metadata keys, not just the defined
1021 * @param array|object $data
1022 * @return array|object
1024 public static function stripMetadata( $data ) {
1025 if ( is_array( $data ) ||
is_object( $data ) ) {
1026 $isObj = is_object( $data );
1028 $data = (array)$data;
1030 $preserveKeys = isset( $data[self
::META_PRESERVE_KEYS
] )
1031 ?
(array)$data[self
::META_PRESERVE_KEYS
]
1033 foreach ( $data as $k => $v ) {
1034 if ( self
::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1036 } elseif ( is_array( $v ) ||
is_object( $v ) ) {
1037 $data[$k] = self
::stripMetadata( $v );
1041 $data = (object)$data;
1048 * Remove metadata keys from a data array or object, non-recursive
1050 * Note this removes all potential metadata keys, not just the defined
1054 * @param array|object $data
1055 * @param array &$metadata Store metadata here, if provided
1056 * @return array|object
1058 public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
1059 if ( !is_array( $metadata ) ) {
1062 if ( is_array( $data ) ||
is_object( $data ) ) {
1063 $isObj = is_object( $data );
1065 $data = (array)$data;
1067 $preserveKeys = isset( $data[self
::META_PRESERVE_KEYS
] )
1068 ?
(array)$data[self
::META_PRESERVE_KEYS
]
1070 foreach ( $data as $k => $v ) {
1071 if ( self
::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1077 $data = (object)$data;
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())
1089 private static function size( $value ) {
1091 if ( is_array( $value ) ) {
1092 foreach ( $value as $k => $v ) {
1093 if ( !self
::isMetadataKey( $k ) ) {
1094 $s +
= self
::size( $v );
1097 } elseif ( is_scalar( $value ) ) {
1098 $s = strlen( $value );
1105 * Return a reference to the internal data at $path
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.
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 ) {
1125 $ret = [ $k => [] ] +
$ret;
1131 $fail = implode( '.', array_slice( $path, 0, $i +
1 ) );
1132 throw new InvalidArgumentException( "Path $fail does not exist" );
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" );
1145 * Add the correct metadata to an array of vars we want to export through
1148 * @param array $vars
1149 * @param bool $forceHash
1152 public static function addMetadataToResultVars( $vars, $forceHash = true ) {
1153 // Process subarrays and determine if this is a JS [] or {}
1157 foreach ( $vars as $k => $v ) {
1158 if ( is_array( $v ) ||
is_object( $v ) ) {
1159 $vars[$k] = ApiResult
::addMetadataToResultVars( (array)$v, is_object( $v ) );
1160 } elseif ( is_bool( $v ) ) {
1161 // Better here to use real bools even in BC formats
1164 if ( is_string( $k ) ) {
1166 } elseif ( $k > $maxKey ) {
1170 if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
1174 // Set metadata appropriately
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 ApiResult
::META_TYPE
, ApiResult
::META_PRESERVE_KEYS
, ApiResult
::META_KVP_KEY_NAME
,
1180 ApiResult
::META_INDEXED_TAG_NAME
, ApiResult
::META_BC_BOOLS
1184 ApiResult
::META_TYPE
=> 'kvp',
1185 ApiResult
::META_KVP_KEY_NAME
=> 'key',
1186 ApiResult
::META_PRESERVE_KEYS
=> $keys,
1187 ApiResult
::META_BC_BOOLS
=> $bools,
1188 ApiResult
::META_INDEXED_TAG_NAME
=> 'var',
1192 ApiResult
::META_TYPE
=> 'array',
1193 ApiResult
::META_BC_BOOLS
=> $bools,
1194 ApiResult
::META_INDEXED_TAG_NAME
=> 'value',
1200 * Format an expiry timestamp for API output
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
1207 public static function formatExpiry( $expiry, $infinity = 'infinity' ) {
1209 if ( $dbInfinity === null ) {
1210 $dbInfinity = wfGetDB( DB_REPLICA
)->getInfinity();
1213 if ( $expiry === '' ||
$expiry === null ||
$expiry === false ||
1214 wfIsInfinity( $expiry ) ||
$expiry === $dbInfinity
1218 return wfTimestamp( TS_ISO_8601
, $expiry );
1227 * For really cool vim folding this needs to be at the end:
1228 * vim: foldmarker=@{,@} foldmethod=marker