150 private $errorFormatter;
156 $this->maxSize = $maxSize;
165 $this->errorFormatter = $formatter;
186 self::META_TYPE =>
'assoc',
247 return self::applyTransformations( $this->data, $transforms );
250 $last = array_pop(
$path );
251 $ret = &$this->path(
$path,
'dummy' );
252 if ( !isset( $ret[$last] ) ) {
254 } elseif ( is_array( $ret[$last] ) ) {
255 return self::applyTransformations( $ret[$last], $transforms );
281 public static function setValue( array &$arr, $name, $value, $flags = 0 ) {
282 if ( ( $flags & self::NO_VALIDATE ) !== self::NO_VALIDATE ) {
283 $value = self::validateValue( $value );
286 if ( $name ===
null ) {
287 if ( $flags & self::ADD_ON_TOP ) {
288 array_unshift( $arr, $value );
295 $exists = isset( $arr[$name] );
296 if ( !$exists || ( $flags & self::OVERRIDE ) ) {
297 if ( !$exists && ( $flags & self::ADD_ON_TOP ) ) {
298 $arr = [ $name => $value ] + $arr;
300 $arr[$name] = $value;
302 } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
303 $conflicts = array_intersect_key( $arr[$name], $value );
305 $arr[$name] += $value;
307 $keys = implode(
', ', array_keys( $conflicts ) );
308 throw new RuntimeException(
309 "Conflicting keys ($keys) when attempting to merge element $name"
312 } elseif ( $value !== $arr[$name] ) {
313 throw new RuntimeException(
314 "Attempting to add element $name=$value, existing value is {$arr[$name]}"
324 private static function validateValue( $value ) {
325 if ( is_object( $value ) ) {
328 if ( is_callable( [ $value,
'serializeForApiResult' ] ) ) {
330 $value = $value->serializeForApiResult();
331 if ( is_object( $value ) ) {
332 throw new UnexpectedValueException(
333 get_class( $oldValue ) .
'::serializeForApiResult() returned an object of class ' .
341 return self::validateValue( $value );
342 }
catch ( Exception $ex ) {
343 throw new UnexpectedValueException(
344 get_class( $oldValue ) .
'::serializeForApiResult() returned an invalid value: ' .
350 } elseif ( is_callable( [ $value,
'__toString' ] ) ) {
351 $value = (string)$value;
353 $value = (array)$value + [ self::META_TYPE =>
'assoc' ];
357 if ( is_string( $value ) ) {
359 static $contentLanguage =
null;
360 if ( !$contentLanguage ) {
361 $contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
363 $value = $contentLanguage->normalize( $value );
364 } elseif ( is_array( $value ) ) {
365 foreach ( $value as $k => $v ) {
366 $value[$k] = self::validateValue( $v );
368 } elseif ( $value !==
null && !is_scalar( $value ) ) {
369 $type = gettype( $value );
371 if ( is_resource( $value ) ) {
372 $type .=
'(' . get_resource_type( $value ) .
')';
374 throw new InvalidArgumentException(
"Cannot add $type to ApiResult" );
375 } elseif ( is_float( $value ) && !is_finite( $value ) ) {
376 throw new InvalidArgumentException(
'Cannot add non-finite floats to ApiResult' );
399 $arr = &$this->path(
$path, ( $flags & self::ADD_ON_TOP ) ?
'prepend' :
'append' );
401 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
404 $value = self::validateValue( $value );
405 $flags |= self::NO_VALIDATE;
407 $newsize = $this->size + self::size( $value );
408 if ( $this->maxSize !==
false && $newsize > $this->maxSize ) {
409 $this->errorFormatter->addWarning(
410 'result', [
'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ]
414 $this->size = $newsize;
417 self::setValue( $arr, $name, $value, $flags );
429 if ( isset( $arr[$name] ) ) {
431 unset( $arr[$name] );
448 if ( $name ===
null ) {
450 throw new InvalidArgumentException(
'Cannot remove the data root' );
452 $name = array_pop(
$path );
454 $ret = self::unsetValue( $this->path(
$path,
'dummy' ), $name );
455 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
456 $newsize = $this->size - self::size( $ret );
457 $this->size = max( $newsize, 0 );
472 if ( $name ===
null ) {
473 throw new InvalidArgumentException(
'Content value must be named' );
475 self::setContentField( $arr, $name, $flags );
476 self::setValue( $arr, $name, $value, $flags );
490 if ( $name ===
null ) {
491 throw new InvalidArgumentException(
'Content value must be named' );
506 $this->
addValue(
'limits', $moduleName, $limit,
507 self::OVERRIDE | self::NO_SIZE_CHECK );
525 if ( isset( $arr[self::META_CONTENT] ) &&
526 isset( $arr[$arr[self::META_CONTENT]] ) &&
527 !( $flags & self::OVERRIDE )
529 throw new RuntimeException(
530 "Attempting to set content element as $name when " . $arr[self::META_CONTENT] .
531 ' is already set as the content element'
534 $arr[self::META_CONTENT] = $name;
546 $arr = &$this->path(
$path, ( $flags & self::ADD_ON_TOP ) ?
'prepend' :
'append' );
547 self::setContentField( $arr, $name, $flags );
558 if ( !isset( $arr[self::META_SUBELEMENTS] ) ) {
559 $arr[self::META_SUBELEMENTS] = (array)$names;
561 $arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names );
573 $arr = &$this->path(
$path );
574 self::setSubelementsList( $arr, $names );
585 if ( isset( $arr[self::META_SUBELEMENTS] ) ) {
586 $arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names );
598 $arr = &$this->path(
$path );
599 self::unsetSubelementsList( $arr, $names );
609 if ( !is_string( $tag ) ) {
610 throw new InvalidArgumentException(
'Bad tag name' );
612 $arr[self::META_INDEXED_TAG_NAME] = $tag;
622 $arr = &$this->path(
$path );
623 self::setIndexedTagName( $arr, $tag );
634 if ( !is_string( $tag ) ) {
635 throw new InvalidArgumentException(
'Bad tag name' );
637 $arr[self::META_INDEXED_TAG_NAME] = $tag;
638 foreach ( $arr as $k => &$v ) {
639 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
640 self::setIndexedTagNameRecursive( $v, $tag );
653 $arr = &$this->path(
$path );
654 self::setIndexedTagNameRecursive( $arr, $tag );
668 if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) {
669 $arr[self::META_PRESERVE_KEYS] = (array)$names;
671 $arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names );
683 $arr = &$this->path(
$path );
684 self::setPreserveKeysList( $arr, $names );
695 if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) {
696 $arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names );
708 $arr = &$this->path(
$path );
709 self::unsetPreserveKeysList( $arr, $names );
720 public static function setArrayType( array &$arr, $type, $kvpKeyName =
null ) {
721 if ( !in_array( $type, [
722 'default',
'array',
'assoc',
'kvp',
'BCarray',
'BCassoc',
'BCkvp'
724 throw new InvalidArgumentException(
'Bad type' );
726 $arr[self::META_TYPE] = $type;
727 if ( is_string( $kvpKeyName ) ) {
728 $arr[self::META_KVP_KEY_NAME] = $kvpKeyName;
740 $arr = &$this->path(
$path );
741 self::setArrayType( $arr, $tag, $kvpKeyName );
752 self::setArrayType( $arr, $type, $kvpKeyName );
753 foreach ( $arr as $k => &$v ) {
754 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
755 self::setArrayTypeRecursive( $v, $type, $kvpKeyName );
768 $arr = &$this->path(
$path );
769 self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName );
788 return ord( $key ) === 95;
801 $strip = $transforms[
'Strip'] ??
'none';
802 if ( $strip ===
'base' ) {
803 $transforms[
'Strip'] =
'none';
805 $transformTypes = $transforms[
'Types'] ??
null;
806 if ( $transformTypes !==
null && !is_array( $transformTypes ) ) {
807 throw new InvalidArgumentException( __METHOD__ .
':Value for "Types" must be an array' );
811 $data = self::stripMetadataNonRecursive( $dataIn, $metadata );
813 if ( isset( $transforms[
'Custom'] ) ) {
814 if ( !is_callable( $transforms[
'Custom'] ) ) {
815 throw new InvalidArgumentException( __METHOD__ .
': Value for "Custom" must be callable' );
817 call_user_func_array( $transforms[
'Custom'], [ &$data, &$metadata ] );
820 if ( ( isset( $transforms[
'BC'] ) || $transformTypes !==
null ) &&
821 isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] ===
'BCkvp' &&
822 !isset( $metadata[self::META_KVP_KEY_NAME] )
824 throw new UnexpectedValueException(
'Type "BCkvp" used without setting ' .
825 'ApiResult::META_KVP_KEY_NAME metadata item' );
830 if ( isset( $transforms[
'BC'] ) ) {
831 if ( !is_array( $transforms[
'BC'] ) ) {
832 throw new InvalidArgumentException( __METHOD__ .
':Value for "BC" must be an array' );
834 if ( !in_array(
'nobool', $transforms[
'BC'],
true ) ) {
835 $boolKeys = isset( $metadata[self::META_BC_BOOLS] )
836 ? array_fill_keys( $metadata[self::META_BC_BOOLS],
true )
840 if ( !in_array(
'no*', $transforms[
'BC'],
true ) &&
841 isset( $metadata[self::META_CONTENT] ) && $metadata[self::META_CONTENT] !==
'*'
843 $k = $metadata[self::META_CONTENT];
844 $data[
'*'] = $data[$k];
846 $metadata[self::META_CONTENT] =
'*';
849 if ( !in_array(
'nosub', $transforms[
'BC'],
true ) &&
850 isset( $metadata[self::META_BC_SUBELEMENTS] )
852 foreach ( $metadata[self::META_BC_SUBELEMENTS] as $k ) {
853 if ( isset( $data[$k] ) ) {
856 self::META_CONTENT =>
'*',
857 self::META_TYPE =>
'assoc',
863 if ( isset( $metadata[self::META_TYPE] ) ) {
864 switch ( $metadata[self::META_TYPE] ) {
867 $metadata[self::META_TYPE] =
'default';
870 $transformTypes[
'ArmorKVP'] = $metadata[self::META_KVP_KEY_NAME];
877 $defaultType =
'array';
879 foreach ( $data as $k => &$v ) {
880 $v = is_array( $v ) ? self::applyTransformations( $v, $transforms ) : $v;
881 if ( $boolKeys !==
null && is_bool( $v ) && !isset( $boolKeys[$k] ) ) {
888 if ( is_string( $k ) ) {
889 $defaultType =
'assoc';
890 } elseif ( $k > $maxKey ) {
903 $keepMetadata = &$metadata;
907 $keepMetadata = array_intersect_key( $metadata, [
908 self::META_INDEXED_TAG_NAME => 1,
909 self::META_SUBELEMENTS => 1,
913 throw new InvalidArgumentException( __METHOD__ .
': Unknown value for "Strip"' );
917 if ( $transformTypes ===
null ) {
918 return $data + $keepMetadata;
921 if ( $defaultType ===
'array' && $maxKey !== count( $data ) - 1 ) {
922 $defaultType =
'assoc';
926 $type = $defaultType;
927 if ( isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] !==
'default' ) {
928 $type = $metadata[self::META_TYPE];
930 if ( ( $type ===
'kvp' || $type ===
'BCkvp' ) &&
931 empty( $transformTypes[
'ArmorKVP'] )
934 } elseif ( $type ===
'BCarray' ) {
936 } elseif ( $type ===
'BCassoc' ) {
943 $metadata[self::META_TYPE] =
'assoc';
944 $data += $keepMetadata;
945 return empty( $transformTypes[
'AssocAsObject'] ) ? $data : (object)$data;
957 uksort( $data,
static function ( $a, $b ):
int {
963 if ( is_numeric( trim( $a ) ) xor is_numeric( trim( $b ) ) ) {
964 return (
string)$a <=> (
string)$b;
970 $data = array_values( $data );
971 $metadata[self::META_TYPE] =
'array';
973 return $data + $keepMetadata;
977 $key = $metadata[self::META_KVP_KEY_NAME] ?? $transformTypes[
'ArmorKVP'];
978 $valKey = isset( $transforms[
'BC'] ) ?
'*' :
'value';
979 $assocAsObject = !empty( $transformTypes[
'AssocAsObject'] );
980 $merge = !empty( $metadata[self::META_KVP_MERGE] );
983 foreach ( $data as $k => $v ) {
984 if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) {
986 if ( isset( $vArr[self::META_TYPE] ) ) {
987 $mergeType = $vArr[self::META_TYPE];
988 } elseif ( is_object( $v ) ) {
989 $mergeType =
'assoc';
991 $keys = array_keys( $vArr );
992 sort( $keys, SORT_NUMERIC );
993 $mergeType = ( $keys === array_keys( $keys ) ) ?
'array' :
'assoc';
998 if ( $mergeType ===
'assoc' ) {
1003 if ( $strip ===
'none' ) {
1004 self::setPreserveKeysList( $item, [ $key ] );
1011 if ( $strip ===
'none' ) {
1013 self::META_PRESERVE_KEYS => [ $key ],
1014 self::META_CONTENT => $valKey,
1015 self::META_TYPE =>
'assoc',
1019 $ret[] = $assocAsObject ? (object)$item : $item;
1021 $metadata[self::META_TYPE] =
'array';
1024 return $ret + $keepMetadata;
1027 throw new UnexpectedValueException(
"Unknown type '$type'" );
1042 if ( is_array( $data ) || is_object( $data ) ) {
1043 $isObj = is_object( $data );
1045 $data = (array)$data;
1047 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1048 ? (array)$data[self::META_PRESERVE_KEYS]
1050 foreach ( $data as $k => $v ) {
1051 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys,
true ) ) {
1053 } elseif ( is_array( $v ) || is_object( $v ) ) {
1054 $data[$k] = self::stripMetadata( $v );
1058 $data = (object)$data;
1076 if ( !is_array( $metadata ) ) {
1079 if ( is_array( $data ) || is_object( $data ) ) {
1080 $isObj = is_object( $data );
1082 $data = (array)$data;
1084 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1085 ? (array)$data[self::META_PRESERVE_KEYS]
1087 foreach ( $data as $k => $v ) {
1088 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys,
true ) ) {
1094 $data = (object)$data;
1106 private static function size( $value ) {
1108 if ( is_array( $value ) ) {
1109 foreach ( $value as $k => $v ) {
1110 if ( !self::isMetadataKey( $k ) ) {
1111 $s += self::size( $v );
1114 } elseif ( is_scalar( $value ) ) {
1115 $s = strlen( $value );
1132 private function &path(
$path, $create =
'append' ) {
1134 $ret = &$this->data;
1135 foreach (
$path as $i => $k ) {
1136 if ( !isset( $ret[$k] ) ) {
1137 switch ( $create ) {
1142 $ret = [ $k => [] ] + $ret;
1148 $fail = implode(
'.', array_slice(
$path, 0, $i + 1 ) );
1149 throw new InvalidArgumentException(
"Path $fail does not exist" );
1152 if ( !is_array( $ret[$k] ) ) {
1153 $fail = implode(
'.', array_slice(
$path, 0, $i + 1 ) );
1154 throw new InvalidArgumentException(
"Path $fail is not an array" );
1174 foreach ( $vars as $k => $v ) {
1175 if ( is_array( $v ) || is_object( $v ) ) {
1176 $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) );
1177 } elseif ( is_bool( $v ) ) {
1181 if ( is_string( $k ) ) {
1183 } elseif ( $k > $maxKey ) {
1187 if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
1195 $keys = array_diff( array_keys( $vars ), [
1196 self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME,
1197 self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS
1201 self::META_TYPE =>
'kvp',
1202 self::META_KVP_KEY_NAME =>
'key',
1203 self::META_PRESERVE_KEYS => $keys,
1204 self::META_BC_BOOLS => $bools,
1205 self::META_INDEXED_TAG_NAME =>
'var',
1209 self::META_TYPE =>
'array',
1210 self::META_BC_BOOLS => $bools,
1211 self::META_INDEXED_TAG_NAME =>
'value',
1226 $dbInfinity ??= MediaWikiServices::getInstance()->getConnectionProvider()
1227 ->getReplicaDatabase()
1230 if ( $expiry ===
'' || $expiry ===
null || $expiry ===
false ||
wfIsInfinity( $str)
Determine input string is represents as infinity.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
This class represents the result of the API operations.
addArrayType( $path, $tag, $kvpKeyName=null)
Set the array data type for a path.
static unsetSubelementsList(array &$arr, $names)
Causes the elements with the specified names to be output as attributes (when possible) rather than a...
static unsetPreserveKeysList(array &$arr, $names)
Don't preserve specified keys.
static applyTransformations(array $dataIn, array $transforms)
Apply transformations to an array, returning the transformed array.
const META_TYPE
Key for the 'type' metadata item.
static stripMetadataNonRecursive( $data, &$metadata=null)
Remove metadata keys from a data array or object, non-recursive.
static setArrayType(array &$arr, $type, $kvpKeyName=null)
Set the array data type.
serializeForApiResult()
Allow for adding one ApiResult into another.
static addMetadataToResultVars( $vars, $forceHash=true)
Add the correct metadata to an array of vars we want to export through the API.
static setValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name.
const META_SUBELEMENTS
Key for the 'subelements' metadata item.
addIndexedTagName( $path, $tag)
Set the tag name for numeric-keyed values in XML format.
addValue( $path, $name, $value, $flags=0)
Add value to the output data at the given path.
const META_BC_BOOLS
Key for the 'BC bools' metadata item.
const META_PRESERVE_KEYS
Key for the 'preserve keys' metadata item.
const NO_SIZE_CHECK
For addValue() and similar functions, do not check size while adding a value Don't use this unless yo...
addArrayTypeRecursive( $path, $tag, $kvpKeyName=null)
Set the array data type for a path recursively.
getSize()
Get the size of the result, i.e.
static unsetValue(array &$arr, $name)
Remove an output value to the array by name.
static setPreserveKeysList(array &$arr, $names)
Preserve specified keys.
addPreserveKeysList( $path, $names)
Preserve specified keys.
addParsedLimit( $moduleName, $limit)
Add the numeric limit for a limit=max to the result.
addSubelementsList( $path, $names)
Causes the elements with the specified names to be output as subelements rather than attributes.
static stripMetadata( $data)
Recursively remove metadata keys from a data array or object.
const META_CONTENT
Key for the 'content' metadata item.
addIndexedTagNameRecursive( $path, $tag)
Set indexed tag name on $path and all subarrays.
removeSubelementsList( $path, $names)
Causes the elements with the specified names to be output as attributes (when possible) rather than a...
setErrorFormatter(ApiErrorFormatter $formatter)
const OVERRIDE
Override existing value in addValue(), setValue(), and similar functions.
static setSubelementsList(array &$arr, $names)
Causes the elements with the specified names to be output as subelements rather than attributes.
static setArrayTypeRecursive(array &$arr, $type, $kvpKeyName=null)
Set the array data type recursively.
const META_KVP_KEY_NAME
Key for the metadata item whose value specifies the name used for the kvp key in the alternative outp...
removeValue( $path, $name, $flags=0)
Remove value from the output data at the given path.
const ADD_ON_TOP
For addValue(), setValue() and similar functions, if the value does not exist, add it as the first el...
getResultData( $path=[], $transforms=[])
Get the result data array.
const META_BC_SUBELEMENTS
Key for the 'BC subelements' metadata item.
removePreserveKeysList( $path, $names)
Don't preserve specified keys.
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
const META_INDEXED_TAG_NAME
Key for the 'indexed tag name' metadata item.
const META_KVP_MERGE
Key for the metadata item that indicates that the KVP key should be added into an assoc value,...
reset()
Clear the current result data.
static setContentValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name and mark as META_CONTENT.
static setIndexedTagNameRecursive(array &$arr, $tag)
Set indexed tag name on $arr and all subarrays.
static setContentField(array &$arr, $name, $flags=0)
Set the name of the content field name (META_CONTENT)
static formatExpiry( $expiry, $infinity='infinity')
Format an expiry timestamp for API output.
const NO_VALIDATE
For addValue(), setValue() and similar functions, do not validate data.
addContentValue( $path, $name, $value, $flags=0)
Add value to the output data at the given path and mark as META_CONTENT.
addContentField( $path, $name, $flags=0)
Set the name of the content field name (META_CONTENT)
static isMetadataKey( $key)
Test whether a key should be considered metadata.
This interface allows for overriding the default conversion applied by ApiResult::validateValue().