MediaWiki master
ApiResult.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Api;
22
23use Exception;
24use InvalidArgumentException;
27use RuntimeException;
28use stdClass;
29use UnexpectedValueException;
30
43class ApiResult implements ApiSerializable {
44
49 public const OVERRIDE = 1;
50
57 public const ADD_ON_TOP = 2;
58
66 public const NO_SIZE_CHECK = 4;
67
74 public const NO_VALIDATE = self::NO_SIZE_CHECK | 8;
75
80 public const META_INDEXED_TAG_NAME = '_element';
81
86 public const META_SUBELEMENTS = '_subelements';
87
92 public const META_PRESERVE_KEYS = '_preservekeys';
93
98 public const META_CONTENT = '_content';
99
118 public const META_TYPE = '_type';
119
127 public const META_KVP_KEY_NAME = '_kvpkeyname';
128
137 public const META_KVP_MERGE = '_kvpmerge';
138
144 public const META_BC_BOOLS = '_BC_bools';
145
151 public const META_BC_SUBELEMENTS = '_BC_subelements';
152
154 private $data;
155 private int $size;
157 private $maxSize;
158 private ApiErrorFormatter $errorFormatter;
159
163 public function __construct( $maxSize ) {
164 $this->maxSize = $maxSize;
165 $this->reset();
166 }
167
172 public function setErrorFormatter( ApiErrorFormatter $formatter ) {
173 $this->errorFormatter = $formatter;
174 }
175
181 public function serializeForApiResult() {
182 return $this->data;
183 }
184
185 /***************************************************************************/
186 // region Content
192 public function reset() {
193 $this->data = [
194 self::META_TYPE => 'assoc', // Usually what's desired
195 ];
196 $this->size = 0;
197 }
198
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
273 public function getSize() {
274 return $this->size;
275 }
276
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 $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 } elseif ( $value !== $arr[$name] ) {
321 throw new RuntimeException(
322 "Attempting to add element $name=$value, existing value is {$arr[$name]}"
323 );
324 }
325 }
326
332 private static function validateValue( $value ) {
333 if ( is_object( $value ) ) {
334 // Note we use is_callable() here instead of instanceof because
335 // ApiSerializable is an informal protocol (see docs there for details).
336 if ( is_callable( [ $value, 'serializeForApiResult' ] ) ) {
337 $oldValue = $value;
338 $value = $value->serializeForApiResult();
339 if ( is_object( $value ) ) {
340 throw new UnexpectedValueException(
341 get_class( $oldValue ) . '::serializeForApiResult() returned an object of class ' .
342 get_class( $value )
343 );
344 }
345
346 // Recursive call instead of fall-through so we can throw a
347 // better exception message.
348 try {
349 return self::validateValue( $value );
350 } catch ( Exception $ex ) {
351 throw new UnexpectedValueException(
352 get_class( $oldValue ) . '::serializeForApiResult() returned an invalid value: ' .
353 $ex->getMessage(),
354 0,
355 $ex
356 );
357 }
358 } elseif ( $value instanceof \Wikimedia\Message\MessageParam ) {
359 // HACK Support code that puts $msg->getParams() directly into API responses
360 // (e.g. ApiErrorFormatter::formatRawMessage()).
361 $codec = MediaWikiServices::getInstance()->getJsonCodec();
362 $value = $value->getType() === 'text' ? $value->getValue() : $codec->serialize( $value );
363 } elseif ( is_callable( [ $value, '__toString' ] ) ) {
364 $value = (string)$value;
365 } else {
366 $value = (array)$value + [ self::META_TYPE => 'assoc' ];
367 }
368 }
369
370 if ( is_string( $value ) ) {
371 // Optimization: avoid querying the service locator for each value.
372 static $contentLanguage = null;
373 if ( !$contentLanguage ) {
374 $contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
375 }
376 $value = $contentLanguage->normalize( $value );
377 } elseif ( is_array( $value ) ) {
378 foreach ( $value as $k => $v ) {
379 $value[$k] = self::validateValue( $v );
380 }
381 } elseif ( $value !== null && !is_scalar( $value ) ) {
382 $type = get_debug_type( $value );
383 throw new InvalidArgumentException( "Cannot add $type to ApiResult" );
384 } elseif ( is_float( $value ) && !is_finite( $value ) ) {
385 throw new InvalidArgumentException( 'Cannot add non-finite floats to ApiResult' );
386 }
387
388 return $value;
389 }
390
407 public function addValue( $path, $name, $value, $flags = 0 ) {
408 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
409
410 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
411 // self::size needs the validated value. Then flag
412 // to not re-validate later.
413 $value = self::validateValue( $value );
414 $flags |= self::NO_VALIDATE;
415
416 $newsize = $this->size + self::size( $value );
417 if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
418 $this->errorFormatter->addWarning(
419 'result', [ 'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ]
420 );
421 return false;
422 }
423 $this->size = $newsize;
424 }
425
426 self::setValue( $arr, $name, $value, $flags );
427 return true;
428 }
429
436 public static function unsetValue( array &$arr, $name ) {
437 $ret = null;
438 if ( isset( $arr[$name] ) ) {
439 $ret = $arr[$name];
440 unset( $arr[$name] );
441 }
442 return $ret;
443 }
444
455 public function removeValue( $path, $name, $flags = 0 ) {
456 $path = (array)$path;
457 if ( $name === null ) {
458 if ( !$path ) {
459 throw new InvalidArgumentException( 'Cannot remove the data root' );
460 }
461 $name = array_pop( $path );
462 }
463 $ret = self::unsetValue( $this->path( $path, 'dummy' ), $name );
464 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
465 $newsize = $this->size - self::size( $ret );
466 $this->size = max( $newsize, 0 );
467 }
468 return $ret;
469 }
470
480 public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) {
481 if ( $name === null ) {
482 throw new InvalidArgumentException( 'Content value must be named' );
483 }
484 self::setContentField( $arr, $name, $flags );
485 self::setValue( $arr, $name, $value, $flags );
486 }
487
498 public function addContentValue( $path, $name, $value, $flags = 0 ) {
499 if ( $name === null ) {
500 throw new InvalidArgumentException( 'Content value must be named' );
501 }
502 $this->addContentField( $path, $name, $flags );
503 return $this->addValue( $path, $name, $value, $flags );
504 }
505
513 public function addParsedLimit( $moduleName, $limit ) {
514 // Add value, allowing overwriting
515 $this->addValue( 'limits', $moduleName, $limit,
516 self::OVERRIDE | self::NO_SIZE_CHECK );
517 }
518
519 // endregion -- end of Content
520
521 /***************************************************************************/
522 // region Metadata
533 public static function setContentField( array &$arr, $name, $flags = 0 ) {
534 if ( isset( $arr[self::META_CONTENT] ) &&
535 isset( $arr[$arr[self::META_CONTENT]] ) &&
536 !( $flags & self::OVERRIDE )
537 ) {
538 throw new RuntimeException(
539 "Attempting to set content element as $name when " . $arr[self::META_CONTENT] .
540 ' is already set as the content element'
541 );
542 }
543 $arr[self::META_CONTENT] = $name;
544 }
545
554 public function addContentField( $path, $name, $flags = 0 ) {
555 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
556 self::setContentField( $arr, $name, $flags );
557 }
558
566 public static function setSubelementsList( array &$arr, $names ) {
567 if ( !isset( $arr[self::META_SUBELEMENTS] ) ) {
568 $arr[self::META_SUBELEMENTS] = (array)$names;
569 } else {
570 $arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names );
571 }
572 }
573
581 public function addSubelementsList( $path, $names ) {
582 $arr = &$this->path( $path );
583 self::setSubelementsList( $arr, $names );
584 }
585
593 public static function unsetSubelementsList( array &$arr, $names ) {
594 if ( isset( $arr[self::META_SUBELEMENTS] ) ) {
595 $arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names );
596 }
597 }
598
606 public function removeSubelementsList( $path, $names ) {
607 $arr = &$this->path( $path );
608 self::unsetSubelementsList( $arr, $names );
609 }
610
617 public static function setIndexedTagName( array &$arr, $tag ) {
618 if ( !is_string( $tag ) ) {
619 throw new InvalidArgumentException( 'Bad tag name' );
620 }
621 $arr[self::META_INDEXED_TAG_NAME] = $tag;
622 }
623
630 public function addIndexedTagName( $path, $tag ) {
631 $arr = &$this->path( $path );
632 self::setIndexedTagName( $arr, $tag );
633 }
634
642 public static function setIndexedTagNameRecursive( array &$arr, $tag ) {
643 if ( !is_string( $tag ) ) {
644 throw new InvalidArgumentException( 'Bad tag name' );
645 }
646 $arr[self::META_INDEXED_TAG_NAME] = $tag;
647 foreach ( $arr as $k => &$v ) {
648 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
650 }
651 }
652 }
653
661 public function addIndexedTagNameRecursive( $path, $tag ) {
662 $arr = &$this->path( $path );
664 }
665
676 public static function setPreserveKeysList( array &$arr, $names ) {
677 if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) {
678 $arr[self::META_PRESERVE_KEYS] = (array)$names;
679 } else {
680 $arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names );
681 }
682 }
683
691 public function addPreserveKeysList( $path, $names ) {
692 $arr = &$this->path( $path );
693 self::setPreserveKeysList( $arr, $names );
694 }
695
703 public static function unsetPreserveKeysList( array &$arr, $names ) {
704 if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) {
705 $arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names );
706 }
707 }
708
716 public function removePreserveKeysList( $path, $names ) {
717 $arr = &$this->path( $path );
718 self::unsetPreserveKeysList( $arr, $names );
719 }
720
729 public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) {
730 if ( !in_array( $type, [
731 'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp'
732 ], true ) ) {
733 throw new InvalidArgumentException( 'Bad type' );
734 }
735 $arr[self::META_TYPE] = $type;
736 if ( is_string( $kvpKeyName ) ) {
737 $arr[self::META_KVP_KEY_NAME] = $kvpKeyName;
738 }
739 }
740
748 public function addArrayType( $path, $tag, $kvpKeyName = null ) {
749 $arr = &$this->path( $path );
750 self::setArrayType( $arr, $tag, $kvpKeyName );
751 }
752
760 public static function setArrayTypeRecursive( array &$arr, $type, $kvpKeyName = null ) {
761 self::setArrayType( $arr, $type, $kvpKeyName );
762 foreach ( $arr as $k => &$v ) {
763 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
764 self::setArrayTypeRecursive( $v, $type, $kvpKeyName );
765 }
766 }
767 }
768
776 public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) {
777 $arr = &$this->path( $path );
778 self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName );
779 }
780
781 // endregion -- end of Metadata
782
783 /***************************************************************************/
784 // region Utility
793 public static function isMetadataKey( $key ) {
794 // Optimization: This is a very hot and highly optimized code path. Note that ord() only
795 // considers the first character and also works with empty strings and integers.
796 // 95 corresponds to the '_' character.
797 return ord( $key ) === 95;
798 }
799
809 protected static function applyTransformations( array $dataIn, array $transforms ) {
810 $strip = $transforms['Strip'] ?? 'none';
811 if ( $strip === 'base' ) {
812 $transforms['Strip'] = 'none';
813 }
814 $transformTypes = $transforms['Types'] ?? null;
815 if ( $transformTypes !== null && !is_array( $transformTypes ) ) {
816 throw new InvalidArgumentException( __METHOD__ . ':Value for "Types" must be an array' );
817 }
818
819 $metadata = [];
820 $data = self::stripMetadataNonRecursive( $dataIn, $metadata );
821
822 if ( isset( $transforms['Custom'] ) ) {
823 if ( !is_callable( $transforms['Custom'] ) ) {
824 throw new InvalidArgumentException( __METHOD__ . ': Value for "Custom" must be callable' );
825 }
826 call_user_func_array( $transforms['Custom'], [ &$data, &$metadata ] );
827 }
828
829 if ( ( isset( $transforms['BC'] ) || $transformTypes !== null ) &&
830 isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] === 'BCkvp' &&
831 !isset( $metadata[self::META_KVP_KEY_NAME] )
832 ) {
833 throw new UnexpectedValueException( 'Type "BCkvp" used without setting ' .
834 'ApiResult::META_KVP_KEY_NAME metadata item' );
835 }
836
837 // BC transformations
838 $boolKeys = null;
839 if ( isset( $transforms['BC'] ) ) {
840 if ( !is_array( $transforms['BC'] ) ) {
841 throw new InvalidArgumentException( __METHOD__ . ':Value for "BC" must be an array' );
842 }
843 if ( !in_array( 'nobool', $transforms['BC'], true ) ) {
844 $boolKeys = isset( $metadata[self::META_BC_BOOLS] )
845 ? array_fill_keys( $metadata[self::META_BC_BOOLS], true )
846 : [];
847 }
848
849 if ( !in_array( 'no*', $transforms['BC'], true ) &&
850 isset( $metadata[self::META_CONTENT] ) && $metadata[self::META_CONTENT] !== '*'
851 ) {
852 $k = $metadata[self::META_CONTENT];
853 $data['*'] = $data[$k];
854 unset( $data[$k] );
855 $metadata[self::META_CONTENT] = '*';
856 }
857
858 if ( !in_array( 'nosub', $transforms['BC'], true ) &&
859 isset( $metadata[self::META_BC_SUBELEMENTS] )
860 ) {
861 foreach ( $metadata[self::META_BC_SUBELEMENTS] as $k ) {
862 if ( isset( $data[$k] ) ) {
863 $data[$k] = [
864 '*' => $data[$k],
865 self::META_CONTENT => '*',
866 self::META_TYPE => 'assoc',
867 ];
868 }
869 }
870 }
871
872 if ( isset( $metadata[self::META_TYPE] ) ) {
873 switch ( $metadata[self::META_TYPE] ) {
874 case 'BCarray':
875 case 'BCassoc':
876 $metadata[self::META_TYPE] = 'default';
877 break;
878 case 'BCkvp':
879 $transformTypes['ArmorKVP'] = $metadata[self::META_KVP_KEY_NAME];
880 break;
881 }
882 }
883 }
884
885 // Figure out type, do recursive calls, and do boolean transform if necessary
886 $defaultType = 'array';
887 $maxKey = -1;
888 foreach ( $data as $k => &$v ) {
889 $v = is_array( $v ) ? self::applyTransformations( $v, $transforms ) : $v;
890 if ( $boolKeys !== null && is_bool( $v ) && !isset( $boolKeys[$k] ) ) {
891 if ( !$v ) {
892 unset( $data[$k] );
893 continue;
894 }
895 $v = '';
896 }
897 if ( is_string( $k ) ) {
898 $defaultType = 'assoc';
899 } elseif ( $k > $maxKey ) {
900 $maxKey = $k;
901 }
902 }
903 unset( $v );
904
905 // Determine which metadata to keep
906 switch ( $strip ) {
907 case 'all':
908 case 'base':
909 $keepMetadata = [];
910 break;
911 case 'none':
912 $keepMetadata = &$metadata;
913 break;
914 case 'bc':
915 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal Type mismatch on pass-by-ref args
916 $keepMetadata = array_intersect_key( $metadata, [
917 self::META_INDEXED_TAG_NAME => 1,
918 self::META_SUBELEMENTS => 1,
919 ] );
920 break;
921 default:
922 throw new InvalidArgumentException( __METHOD__ . ': Unknown value for "Strip"' );
923 }
924
925 // No type transformation
926 if ( $transformTypes === null ) {
927 return $data + $keepMetadata;
928 }
929
930 if ( $defaultType === 'array' && $maxKey !== count( $data ) - 1 ) {
931 $defaultType = 'assoc';
932 }
933
934 // Override type, if provided
935 $type = $defaultType;
936 if ( isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] !== 'default' ) {
937 $type = $metadata[self::META_TYPE];
938 }
939 if ( ( $type === 'kvp' || $type === 'BCkvp' ) &&
940 empty( $transformTypes['ArmorKVP'] )
941 ) {
942 $type = 'assoc';
943 } elseif ( $type === 'BCarray' ) {
944 $type = 'array';
945 } elseif ( $type === 'BCassoc' ) {
946 $type = 'assoc';
947 }
948
949 // Apply transformation
950 switch ( $type ) {
951 case 'assoc':
952 $metadata[self::META_TYPE] = 'assoc';
953 $data += $keepMetadata;
954 return empty( $transformTypes['AssocAsObject'] ) ? $data : (object)$data;
955
956 case 'array':
957 // Sort items in ascending order by key. Note that $data may contain a mix of number and string keys,
958 // for which the sorting behavior of krsort() with SORT_REGULAR is inconsistent between PHP versions.
959 // Given a comparison of a string key and a number key, PHP < 8.2 coerces the string key into a number
960 // (which yields zero if the string was non-numeric), and then performs the comparison,
961 // while PHP >= 8.2 makes the behavior consistent with stricter numeric comparisons introduced by
962 // PHP 8.0 in that if the string key is non-numeric, it converts the number key into a string
963 // and compares those two strings instead. We therefore use a custom comparison function
964 // implementing PHP >= 8.2 ordering semantics to ensure consistent ordering of items
965 // irrespective of the PHP version (T326480).
966 uksort( $data, static function ( $a, $b ): int {
967 // In a comparison of a number or numeric string with a non-numeric string,
968 // coerce both values into a string prior to comparing and compare the resulting strings.
969 // Note that PHP prior to 8.0 did not consider numeric strings with trailing whitespace
970 // to be numeric, so trim the inputs prior to the numeric checks to make the behavior
971 // consistent across PHP versions.
972 if ( is_numeric( trim( $a ) ) xor is_numeric( trim( $b ) ) ) {
973 return (string)$a <=> (string)$b;
974 }
975
976 return $a <=> $b;
977 } );
978
979 $data = array_values( $data );
980 $metadata[self::META_TYPE] = 'array';
981 // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
982 return $data + $keepMetadata;
983
984 case 'kvp':
985 case 'BCkvp':
986 $key = $metadata[self::META_KVP_KEY_NAME] ?? $transformTypes['ArmorKVP'];
987 $valKey = isset( $transforms['BC'] ) ? '*' : 'value';
988 $assocAsObject = !empty( $transformTypes['AssocAsObject'] );
989 $merge = !empty( $metadata[self::META_KVP_MERGE] );
990
991 $ret = [];
992 foreach ( $data as $k => $v ) {
993 if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) {
994 $vArr = (array)$v;
995 if ( isset( $vArr[self::META_TYPE] ) ) {
996 $mergeType = $vArr[self::META_TYPE];
997 } elseif ( is_object( $v ) ) {
998 $mergeType = 'assoc';
999 } else {
1000 $keys = array_keys( $vArr );
1001 sort( $keys, SORT_NUMERIC );
1002 $mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc';
1003 }
1004 } else {
1005 $mergeType = 'n/a';
1006 }
1007 if ( $mergeType === 'assoc' ) {
1008 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable vArr set when used
1009 $item = $vArr + [
1010 $key => $k,
1011 ];
1012 if ( $strip === 'none' ) {
1013 self::setPreserveKeysList( $item, [ $key ] );
1014 }
1015 } else {
1016 $item = [
1017 $key => $k,
1018 $valKey => $v,
1019 ];
1020 if ( $strip === 'none' ) {
1021 $item += [
1022 self::META_PRESERVE_KEYS => [ $key ],
1023 self::META_CONTENT => $valKey,
1024 self::META_TYPE => 'assoc',
1025 ];
1026 }
1027 }
1028 $ret[] = $assocAsObject ? (object)$item : $item;
1029 }
1030 $metadata[self::META_TYPE] = 'array';
1031
1032 // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
1033 return $ret + $keepMetadata;
1034
1035 default:
1036 throw new UnexpectedValueException( "Unknown type '$type'" );
1037 }
1038 }
1039
1050 public static function stripMetadata( $data ) {
1051 if ( is_array( $data ) || is_object( $data ) ) {
1052 $isObj = is_object( $data );
1053 if ( $isObj ) {
1054 $data = (array)$data;
1055 }
1056 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1057 ? (array)$data[self::META_PRESERVE_KEYS]
1058 : [];
1059 foreach ( $data as $k => $v ) {
1060 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1061 unset( $data[$k] );
1062 } elseif ( is_array( $v ) || is_object( $v ) ) {
1063 $data[$k] = self::stripMetadata( $v );
1064 }
1065 }
1066 if ( $isObj ) {
1067 $data = (object)$data;
1068 }
1069 }
1070 return $data;
1071 }
1072
1084 public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
1085 if ( !is_array( $metadata ) ) {
1086 $metadata = [];
1087 }
1088 if ( is_array( $data ) || is_object( $data ) ) {
1089 $isObj = is_object( $data );
1090 if ( $isObj ) {
1091 $data = (array)$data;
1092 }
1093 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1094 ? (array)$data[self::META_PRESERVE_KEYS]
1095 : [];
1096 foreach ( $data as $k => $v ) {
1097 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1098 $metadata[$k] = $v;
1099 unset( $data[$k] );
1100 }
1101 }
1102 if ( $isObj ) {
1103 $data = (object)$data;
1104 }
1105 }
1106 return $data;
1107 }
1108
1115 private static function size( $value ) {
1116 $s = 0;
1117 if ( is_array( $value ) ) {
1118 foreach ( $value as $k => $v ) {
1119 if ( !self::isMetadataKey( $k ) ) {
1120 $s += self::size( $v );
1121 }
1122 }
1123 } elseif ( is_scalar( $value ) ) {
1124 $s = strlen( $value );
1125 }
1126
1127 return $s;
1128 }
1129
1141 private function &path( $path, $create = 'append' ) {
1142 $path = (array)$path;
1143 $ret = &$this->data;
1144 foreach ( $path as $i => $k ) {
1145 if ( !isset( $ret[$k] ) ) {
1146 switch ( $create ) {
1147 case 'append':
1148 $ret[$k] = [];
1149 break;
1150 case 'prepend':
1151 $ret = [ $k => [] ] + $ret;
1152 break;
1153 case 'dummy':
1154 $tmp = [];
1155 return $tmp;
1156 default:
1157 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1158 throw new InvalidArgumentException( "Path $fail does not exist" );
1159 }
1160 }
1161 if ( !is_array( $ret[$k] ) ) {
1162 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1163 throw new InvalidArgumentException( "Path $fail is not an array" );
1164 }
1165 $ret = &$ret[$k];
1166 }
1167 return $ret;
1168 }
1169
1178 public static function addMetadataToResultVars( $vars, $forceHash = true ) {
1179 // Process subarrays and determine if this is a JS [] or {}
1180 $hash = $forceHash;
1181 $maxKey = -1;
1182 $bools = [];
1183 foreach ( $vars as $k => $v ) {
1184 if ( is_array( $v ) || is_object( $v ) ) {
1185 $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) );
1186 } elseif ( is_bool( $v ) ) {
1187 // Better here to use real bools even in BC formats
1188 $bools[] = $k;
1189 }
1190 if ( is_string( $k ) ) {
1191 $hash = true;
1192 } elseif ( $k > $maxKey ) {
1193 $maxKey = $k;
1194 }
1195 }
1196 if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
1197 $hash = true;
1198 }
1199
1200 // Set metadata appropriately
1201 if ( $hash ) {
1202 // Get the list of keys we actually care about. Unfortunately, we can't support
1203 // certain keys that conflict with ApiResult metadata.
1204 $keys = array_diff( array_keys( $vars ), [
1205 self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME,
1206 self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS
1207 ] );
1208
1209 return [
1210 self::META_TYPE => 'kvp',
1211 self::META_KVP_KEY_NAME => 'key',
1212 self::META_PRESERVE_KEYS => $keys,
1213 self::META_BC_BOOLS => $bools,
1214 self::META_INDEXED_TAG_NAME => 'var',
1215 ] + $vars;
1216 } else {
1217 return [
1218 self::META_TYPE => 'array',
1219 self::META_BC_BOOLS => $bools,
1220 self::META_INDEXED_TAG_NAME => 'value',
1221 ] + $vars;
1222 }
1223 }
1224
1233 public static function formatExpiry( $expiry, $infinity = 'infinity' ) {
1234 static $dbInfinity;
1235 $dbInfinity ??= MediaWikiServices::getInstance()->getConnectionProvider()
1236 ->getReplicaDatabase()
1237 ->getInfinity();
1238
1239 if ( $expiry === '' || $expiry === null || $expiry === false ||
1240 wfIsInfinity( $expiry ) || $expiry === $dbInfinity
1241 ) {
1242 return $infinity;
1243 } else {
1244 return wfTimestamp( TS_ISO_8601, $expiry );
1245 }
1246 }
1247
1248 // endregion -- end of Utility
1249
1250}
1251
1252/*
1253 * This file uses VisualStudio style region/endregion fold markers which are
1254 * recognised by PHPStorm. If modelines are enabled, the following editor
1255 * configuration will also enable folding in vim, if it is in the last 5 lines
1256 * of the file. We also use "@name" which creates sections in Doxygen.
1257 *
1258 * vim: foldmarker=//\ region,//\ endregion foldmethod=marker
1259 */
1260
1262class_alias( ApiResult::class, 'ApiResult' );
wfIsInfinity( $str)
Determine input string is represents as infinity.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Formats errors and warnings for the API, and add them to the associated ApiResult.
This class represents the result of the API operations.
Definition ApiResult.php:43
static setContentField(array &$arr, $name, $flags=0)
Set the name of the content field name (META_CONTENT)
static addMetadataToResultVars( $vars, $forceHash=true)
Add the correct metadata to an array of vars we want to export through the API.
reset()
Clear the current result data.
addParsedLimit( $moduleName, $limit)
Add the numeric limit for a limit=max to the result.
const OVERRIDE
Override existing value in addValue(), setValue(), and similar functions.
Definition ApiResult.php:49
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...
Definition ApiResult.php:57
static applyTransformations(array $dataIn, array $transforms)
Apply transformations to an array, returning the transformed array.
serializeForApiResult()
Allow for adding one ApiResult into another.
addArrayTypeRecursive( $path, $tag, $kvpKeyName=null)
Set the array data type for a path recursively.
addContentField( $path, $name, $flags=0)
Set the name of the content field name (META_CONTENT)
removeSubelementsList( $path, $names)
Causes the elements with the specified names to be output as attributes (when possible) rather than a...
const META_BC_SUBELEMENTS
Key for the 'BC subelements' 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...
Definition ApiResult.php:66
const META_KVP_MERGE
Key for the metadata item that indicates that the KVP key should be added into an assoc value,...
getSize()
Get the size of the result, i.e.
static formatExpiry( $expiry, $infinity='infinity')
Format an expiry timestamp for API output.
addIndexedTagName( $path, $tag)
Set the tag name for numeric-keyed values in XML format.
const META_PRESERVE_KEYS
Key for the 'preserve keys' metadata item.
Definition ApiResult.php:92
addArrayType( $path, $tag, $kvpKeyName=null)
Set the array data type for a path.
const NO_VALIDATE
For addValue(), setValue() and similar functions, do not validate data.
Definition ApiResult.php:74
addSubelementsList( $path, $names)
Causes the elements with the specified names to be output as subelements rather than attributes.
static setValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name.
static unsetSubelementsList(array &$arr, $names)
Causes the elements with the specified names to be output as attributes (when possible) rather than a...
static stripMetadataNonRecursive( $data, &$metadata=null)
Remove metadata keys from a data array or object, non-recursive.
static stripMetadata( $data)
Recursively remove metadata keys from a data array or object.
static isMetadataKey( $key)
Test whether a key should be considered metadata.
static setIndexedTagName(array &$arr, $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.
static unsetValue(array &$arr, $name)
Remove an output value to the array by name.
getResultData( $path=[], $transforms=[])
Get the result data array.
const META_SUBELEMENTS
Key for the 'subelements' metadata item.
Definition ApiResult.php:86
const META_KVP_KEY_NAME
Key for the metadata item whose value specifies the name used for the kvp key in the alternative outp...
const META_INDEXED_TAG_NAME
Key for the 'indexed tag name' metadata item.
Definition ApiResult.php:80
static setPreserveKeysList(array &$arr, $names)
Preserve specified keys.
static setArrayType(array &$arr, $type, $kvpKeyName=null)
Set the array data type.
setErrorFormatter(ApiErrorFormatter $formatter)
addIndexedTagNameRecursive( $path, $tag)
Set indexed tag name on $path and all subarrays.
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 unsetPreserveKeysList(array &$arr, $names)
Don't preserve specified keys.
addPreserveKeysList( $path, $names)
Preserve specified keys.
static setArrayTypeRecursive(array &$arr, $type, $kvpKeyName=null)
Set the array data type recursively.
static setSubelementsList(array &$arr, $names)
Causes the elements with the specified names to be output as subelements rather than attributes.
addContentValue( $path, $name, $value, $flags=0)
Add value to the output data at the given path and mark as META_CONTENT.
removePreserveKeysList( $path, $names)
Don't preserve specified keys.
const META_CONTENT
Key for the 'content' metadata item.
Definition ApiResult.php:98
const META_BC_BOOLS
Key for the 'BC bools' metadata item.
const META_TYPE
Key for the 'type' metadata item.
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:155
This interface allows for overriding the default conversion applied by ApiResult::validateValue().
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...