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;
32
45class ApiResult implements ApiSerializable {
46
51 public const OVERRIDE = 1;
52
59 public const ADD_ON_TOP = 2;
60
68 public const NO_SIZE_CHECK = 4;
69
76 public const NO_VALIDATE = self::NO_SIZE_CHECK | 8;
77
82 public const META_INDEXED_TAG_NAME = '_element';
83
88 public const META_SUBELEMENTS = '_subelements';
89
94 public const META_PRESERVE_KEYS = '_preservekeys';
95
100 public const META_CONTENT = '_content';
101
120 public const META_TYPE = '_type';
121
129 public const META_KVP_KEY_NAME = '_kvpkeyname';
130
139 public const META_KVP_MERGE = '_kvpmerge';
140
146 public const META_BC_BOOLS = '_BC_bools';
147
153 public const META_BC_SUBELEMENTS = '_BC_subelements';
154
156 private $data;
157 private int $size;
159 private $maxSize;
160 private ApiErrorFormatter $errorFormatter;
161
165 public function __construct( $maxSize ) {
166 $this->maxSize = $maxSize;
167 $this->reset();
168 }
169
174 public function setErrorFormatter( ApiErrorFormatter $formatter ) {
175 $this->errorFormatter = $formatter;
176 }
177
183 public function serializeForApiResult() {
184 return $this->data;
185 }
186
187 /***************************************************************************/
188 // region Content
194 public function reset() {
195 $this->data = [
196 self::META_TYPE => 'assoc', // Usually what's desired
197 ];
198 $this->size = 0;
199 }
200
254 public function getResultData( $path = [], $transforms = [] ) {
255 $path = (array)$path;
256 if ( !$path ) {
257 return self::applyTransformations( $this->data, $transforms );
258 }
259
260 $last = array_pop( $path );
261 $ret = &$this->path( $path, 'dummy' );
262 if ( !isset( $ret[$last] ) ) {
263 return null;
264 } elseif ( is_array( $ret[$last] ) ) {
265 return self::applyTransformations( $ret[$last], $transforms );
266 } else {
267 return $ret[$last];
268 }
269 }
270
275 public function getSize() {
276 return $this->size;
277 }
278
291 public static function setValue( array &$arr, $name, $value, $flags = 0 ) {
292 if ( ( $flags & self::NO_VALIDATE ) !== self::NO_VALIDATE ) {
293 $value = self::validateValue( $value );
294 }
295
296 if ( $name === null ) {
297 if ( $flags & self::ADD_ON_TOP ) {
298 array_unshift( $arr, $value );
299 } else {
300 $arr[] = $value;
301 }
302 return;
303 }
304
305 $exists = isset( $arr[$name] );
306 if ( !$exists || ( $flags & self::OVERRIDE ) ) {
307 if ( !$exists && ( $flags & self::ADD_ON_TOP ) ) {
308 $arr = [ $name => $value ] + $arr;
309 } else {
310 $arr[$name] = $value;
311 }
312 } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
313 $conflicts = array_intersect_key( $arr[$name], $value );
314 if ( !$conflicts ) {
315 $arr[$name] += $value;
316 } else {
317 $keys = implode( ', ', array_keys( $conflicts ) );
318 throw new RuntimeException(
319 "Conflicting keys ($keys) when attempting to merge element $name"
320 );
321 }
322 } elseif ( $value !== $arr[$name] ) {
323 throw new RuntimeException(
324 "Attempting to add element $name=$value, existing value is {$arr[$name]}"
325 );
326 }
327 }
328
334 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' ] ) ) {
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 ( $value instanceof ScalarParam || $value instanceof ListParam ) {
361 // HACK Support code that puts $msg->getParams() directly into API responses
362 // (e.g. ApiErrorFormatter::formatRawMessage()).
363 $value = $value->getType() === 'text' ? $value->getValue() : $value->toJsonArray();
364 } elseif ( is_callable( [ $value, '__toString' ] ) ) {
365 $value = (string)$value;
366 } else {
367 $value = (array)$value + [ self::META_TYPE => 'assoc' ];
368 }
369 }
370
371 if ( is_string( $value ) ) {
372 // Optimization: avoid querying the service locator for each value.
373 static $contentLanguage = null;
374 if ( !$contentLanguage ) {
375 $contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
376 }
377 $value = $contentLanguage->normalize( $value );
378 } elseif ( is_array( $value ) ) {
379 foreach ( $value as $k => $v ) {
380 $value[$k] = self::validateValue( $v );
381 }
382 } elseif ( $value !== null && !is_scalar( $value ) ) {
383 $type = get_debug_type( $value );
384 throw new InvalidArgumentException( "Cannot add $type to ApiResult" );
385 } elseif ( is_float( $value ) && !is_finite( $value ) ) {
386 throw new InvalidArgumentException( 'Cannot add non-finite floats to ApiResult' );
387 }
388
389 return $value;
390 }
391
408 public function addValue( $path, $name, $value, $flags = 0 ) {
409 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
410
411 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
412 // self::size needs the validated value. Then flag
413 // to not re-validate later.
414 $value = self::validateValue( $value );
415 $flags |= self::NO_VALIDATE;
416
417 $newsize = $this->size + self::size( $value );
418 if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
419 $this->errorFormatter->addWarning(
420 'result', [ 'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ]
421 );
422 return false;
423 }
424 $this->size = $newsize;
425 }
426
427 self::setValue( $arr, $name, $value, $flags );
428 return true;
429 }
430
437 public static function unsetValue( array &$arr, $name ) {
438 $ret = null;
439 if ( isset( $arr[$name] ) ) {
440 $ret = $arr[$name];
441 unset( $arr[$name] );
442 }
443 return $ret;
444 }
445
456 public function removeValue( $path, $name, $flags = 0 ) {
457 $path = (array)$path;
458 if ( $name === null ) {
459 if ( !$path ) {
460 throw new InvalidArgumentException( 'Cannot remove the data root' );
461 }
462 $name = array_pop( $path );
463 }
464 $ret = self::unsetValue( $this->path( $path, 'dummy' ), $name );
465 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
466 $newsize = $this->size - self::size( $ret );
467 $this->size = max( $newsize, 0 );
468 }
469 return $ret;
470 }
471
481 public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) {
482 if ( $name === null ) {
483 throw new InvalidArgumentException( 'Content value must be named' );
484 }
485 self::setContentField( $arr, $name, $flags );
486 self::setValue( $arr, $name, $value, $flags );
487 }
488
499 public function addContentValue( $path, $name, $value, $flags = 0 ) {
500 if ( $name === null ) {
501 throw new InvalidArgumentException( 'Content value must be named' );
502 }
503 $this->addContentField( $path, $name, $flags );
504 return $this->addValue( $path, $name, $value, $flags );
505 }
506
514 public function addParsedLimit( $moduleName, $limit ) {
515 // Add value, allowing overwriting
516 $this->addValue( 'limits', $moduleName, $limit,
517 self::OVERRIDE | self::NO_SIZE_CHECK );
518 }
519
520 // endregion -- end of Content
521
522 /***************************************************************************/
523 // region Metadata
534 public static function setContentField( array &$arr, $name, $flags = 0 ) {
535 if ( isset( $arr[self::META_CONTENT] ) &&
536 isset( $arr[$arr[self::META_CONTENT]] ) &&
537 !( $flags & self::OVERRIDE )
538 ) {
539 throw new RuntimeException(
540 "Attempting to set content element as $name when " . $arr[self::META_CONTENT] .
541 ' is already set as the content element'
542 );
543 }
544 $arr[self::META_CONTENT] = $name;
545 }
546
555 public function addContentField( $path, $name, $flags = 0 ) {
556 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
557 self::setContentField( $arr, $name, $flags );
558 }
559
567 public static function setSubelementsList( array &$arr, $names ) {
568 if ( !isset( $arr[self::META_SUBELEMENTS] ) ) {
569 $arr[self::META_SUBELEMENTS] = (array)$names;
570 } else {
571 $arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names );
572 }
573 }
574
582 public function addSubelementsList( $path, $names ) {
583 $arr = &$this->path( $path );
584 self::setSubelementsList( $arr, $names );
585 }
586
594 public static function unsetSubelementsList( array &$arr, $names ) {
595 if ( isset( $arr[self::META_SUBELEMENTS] ) ) {
596 $arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names );
597 }
598 }
599
607 public function removeSubelementsList( $path, $names ) {
608 $arr = &$this->path( $path );
609 self::unsetSubelementsList( $arr, $names );
610 }
611
618 public static function setIndexedTagName( array &$arr, $tag ) {
619 if ( !is_string( $tag ) ) {
620 throw new InvalidArgumentException( 'Bad tag name' );
621 }
622 $arr[self::META_INDEXED_TAG_NAME] = $tag;
623 }
624
631 public function addIndexedTagName( $path, $tag ) {
632 $arr = &$this->path( $path );
633 self::setIndexedTagName( $arr, $tag );
634 }
635
643 public static function setIndexedTagNameRecursive( array &$arr, $tag ) {
644 if ( !is_string( $tag ) ) {
645 throw new InvalidArgumentException( 'Bad tag name' );
646 }
647 $arr[self::META_INDEXED_TAG_NAME] = $tag;
648 foreach ( $arr as $k => &$v ) {
649 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
651 }
652 }
653 }
654
662 public function addIndexedTagNameRecursive( $path, $tag ) {
663 $arr = &$this->path( $path );
665 }
666
677 public static function setPreserveKeysList( array &$arr, $names ) {
678 if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) {
679 $arr[self::META_PRESERVE_KEYS] = (array)$names;
680 } else {
681 $arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names );
682 }
683 }
684
692 public function addPreserveKeysList( $path, $names ) {
693 $arr = &$this->path( $path );
694 self::setPreserveKeysList( $arr, $names );
695 }
696
704 public static function unsetPreserveKeysList( array &$arr, $names ) {
705 if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) {
706 $arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names );
707 }
708 }
709
717 public function removePreserveKeysList( $path, $names ) {
718 $arr = &$this->path( $path );
719 self::unsetPreserveKeysList( $arr, $names );
720 }
721
730 public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) {
731 if ( !in_array( $type, [
732 'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp'
733 ], true ) ) {
734 throw new InvalidArgumentException( 'Bad type' );
735 }
736 $arr[self::META_TYPE] = $type;
737 if ( is_string( $kvpKeyName ) ) {
738 $arr[self::META_KVP_KEY_NAME] = $kvpKeyName;
739 }
740 }
741
749 public function addArrayType( $path, $tag, $kvpKeyName = null ) {
750 $arr = &$this->path( $path );
751 self::setArrayType( $arr, $tag, $kvpKeyName );
752 }
753
761 public static function setArrayTypeRecursive( array &$arr, $type, $kvpKeyName = null ) {
762 self::setArrayType( $arr, $type, $kvpKeyName );
763 foreach ( $arr as $k => &$v ) {
764 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
765 self::setArrayTypeRecursive( $v, $type, $kvpKeyName );
766 }
767 }
768 }
769
777 public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) {
778 $arr = &$this->path( $path );
779 self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName );
780 }
781
782 // endregion -- end of Metadata
783
784 /***************************************************************************/
785 // region Utility
794 public static function isMetadataKey( $key ) {
795 // Optimization: This is a very hot and highly optimized code path. Note that ord() only
796 // considers the first character and also works with empty strings and integers.
797 // 95 corresponds to the '_' character.
798 return ord( $key ) === 95;
799 }
800
810 protected static function applyTransformations( array $dataIn, array $transforms ) {
811 $strip = $transforms['Strip'] ?? 'none';
812 if ( $strip === 'base' ) {
813 $transforms['Strip'] = 'none';
814 }
815 $transformTypes = $transforms['Types'] ?? null;
816 if ( $transformTypes !== null && !is_array( $transformTypes ) ) {
817 throw new InvalidArgumentException( __METHOD__ . ':Value for "Types" must be an array' );
818 }
819
820 $metadata = [];
821 $data = self::stripMetadataNonRecursive( $dataIn, $metadata );
822
823 if ( isset( $transforms['Custom'] ) ) {
824 if ( !is_callable( $transforms['Custom'] ) ) {
825 throw new InvalidArgumentException( __METHOD__ . ': Value for "Custom" must be callable' );
826 }
827 $transforms['Custom']( $data, $metadata );
828 }
829
830 if ( ( isset( $transforms['BC'] ) || $transformTypes !== null ) &&
831 isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] === 'BCkvp' &&
832 !isset( $metadata[self::META_KVP_KEY_NAME] )
833 ) {
834 throw new UnexpectedValueException( 'Type "BCkvp" used without setting ' .
835 'ApiResult::META_KVP_KEY_NAME metadata item' );
836 }
837
838 // BC transformations
839 $boolKeys = null;
840 if ( isset( $transforms['BC'] ) ) {
841 if ( !is_array( $transforms['BC'] ) ) {
842 throw new InvalidArgumentException( __METHOD__ . ':Value for "BC" must be an array' );
843 }
844 if ( !in_array( 'nobool', $transforms['BC'], true ) ) {
845 $boolKeys = isset( $metadata[self::META_BC_BOOLS] )
846 ? array_fill_keys( $metadata[self::META_BC_BOOLS], true )
847 : [];
848 }
849
850 if ( !in_array( 'no*', $transforms['BC'], true ) &&
851 isset( $metadata[self::META_CONTENT] ) && $metadata[self::META_CONTENT] !== '*'
852 ) {
853 $k = $metadata[self::META_CONTENT];
854 $data['*'] = $data[$k];
855 unset( $data[$k] );
856 $metadata[self::META_CONTENT] = '*';
857 }
858
859 if ( !in_array( 'nosub', $transforms['BC'], true ) &&
860 isset( $metadata[self::META_BC_SUBELEMENTS] )
861 ) {
862 foreach ( $metadata[self::META_BC_SUBELEMENTS] as $k ) {
863 if ( isset( $data[$k] ) ) {
864 $data[$k] = [
865 '*' => $data[$k],
866 self::META_CONTENT => '*',
867 self::META_TYPE => 'assoc',
868 ];
869 }
870 }
871 }
872
873 if ( isset( $metadata[self::META_TYPE] ) ) {
874 switch ( $metadata[self::META_TYPE] ) {
875 case 'BCarray':
876 case 'BCassoc':
877 $metadata[self::META_TYPE] = 'default';
878 break;
879 case 'BCkvp':
880 $transformTypes['ArmorKVP'] = $metadata[self::META_KVP_KEY_NAME];
881 break;
882 }
883 }
884 }
885
886 // Figure out type, do recursive calls, and do boolean transform if necessary
887 $defaultType = 'array';
888 $maxKey = -1;
889 foreach ( $data as $k => &$v ) {
890 $v = is_array( $v ) ? self::applyTransformations( $v, $transforms ) : $v;
891 if ( $boolKeys !== null && is_bool( $v ) && !isset( $boolKeys[$k] ) ) {
892 if ( !$v ) {
893 unset( $data[$k] );
894 continue;
895 }
896 $v = '';
897 }
898 if ( is_string( $k ) ) {
899 $defaultType = 'assoc';
900 } elseif ( $k > $maxKey ) {
901 $maxKey = $k;
902 }
903 }
904 unset( $v );
905
906 // Determine which metadata to keep
907 switch ( $strip ) {
908 case 'all':
909 case 'base':
910 $keepMetadata = [];
911 break;
912 case 'none':
913 $keepMetadata = &$metadata;
914 break;
915 case 'bc':
916 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal Type mismatch on pass-by-ref args
917 $keepMetadata = array_intersect_key( $metadata, [
918 self::META_INDEXED_TAG_NAME => 1,
919 self::META_SUBELEMENTS => 1,
920 ] );
921 break;
922 default:
923 throw new InvalidArgumentException( __METHOD__ . ': Unknown value for "Strip"' );
924 }
925
926 // No type transformation
927 if ( $transformTypes === null ) {
928 return $data + $keepMetadata;
929 }
930
931 if ( $defaultType === 'array' && $maxKey !== count( $data ) - 1 ) {
932 $defaultType = 'assoc';
933 }
934
935 // Override type, if provided
936 $type = $defaultType;
937 if ( isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] !== 'default' ) {
938 $type = $metadata[self::META_TYPE];
939 }
940 if ( ( $type === 'kvp' || $type === 'BCkvp' ) &&
941 empty( $transformTypes['ArmorKVP'] )
942 ) {
943 $type = 'assoc';
944 } elseif ( $type === 'BCarray' ) {
945 $type = 'array';
946 } elseif ( $type === 'BCassoc' ) {
947 $type = 'assoc';
948 }
949
950 // Apply transformation
951 switch ( $type ) {
952 case 'assoc':
953 $metadata[self::META_TYPE] = 'assoc';
954 $data += $keepMetadata;
955 return empty( $transformTypes['AssocAsObject'] ) ? $data : (object)$data;
956
957 case 'array':
958 // Sort items in ascending order by key. Note that $data may contain a mix of number and string keys,
959 // for which the sorting behavior of krsort() with SORT_REGULAR is inconsistent between PHP versions.
960 // Given a comparison of a string key and a number key, PHP < 8.2 coerces the string key into a number
961 // (which yields zero if the string was non-numeric), and then performs the comparison,
962 // while PHP >= 8.2 makes the behavior consistent with stricter numeric comparisons introduced by
963 // PHP 8.0 in that if the string key is non-numeric, it converts the number key into a string
964 // and compares those two strings instead. We therefore use a custom comparison function
965 // implementing PHP >= 8.2 ordering semantics to ensure consistent ordering of items
966 // irrespective of the PHP version (T326480).
967 uksort( $data, static function ( $a, $b ): int {
968 // In a comparison of a number or numeric string with a non-numeric string,
969 // coerce both values into a string prior to comparing and compare the resulting strings.
970 // Note that PHP prior to 8.0 did not consider numeric strings with trailing whitespace
971 // to be numeric, so trim the inputs prior to the numeric checks to make the behavior
972 // consistent across PHP versions.
973 if ( is_numeric( trim( $a ) ) xor is_numeric( trim( $b ) ) ) {
974 return (string)$a <=> (string)$b;
975 }
976
977 return $a <=> $b;
978 } );
979
980 $data = array_values( $data );
981 $metadata[self::META_TYPE] = 'array';
982 // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
983 return $data + $keepMetadata;
984
985 case 'kvp':
986 case 'BCkvp':
987 $key = $metadata[self::META_KVP_KEY_NAME] ?? $transformTypes['ArmorKVP'];
988 $valKey = isset( $transforms['BC'] ) ? '*' : 'value';
989 $assocAsObject = !empty( $transformTypes['AssocAsObject'] );
990 $merge = !empty( $metadata[self::META_KVP_MERGE] );
991
992 $ret = [];
993 foreach ( $data as $k => $v ) {
994 if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) {
995 $vArr = (array)$v;
996 if ( isset( $vArr[self::META_TYPE] ) ) {
997 $mergeType = $vArr[self::META_TYPE];
998 } elseif ( is_object( $v ) ) {
999 $mergeType = 'assoc';
1000 } else {
1001 $keys = array_keys( $vArr );
1002 sort( $keys, SORT_NUMERIC );
1003 $mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc';
1004 }
1005 } else {
1006 $mergeType = 'n/a';
1007 }
1008 if ( $mergeType === 'assoc' ) {
1009 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable vArr set when used
1010 $item = $vArr + [
1011 $key => $k,
1012 ];
1013 if ( $strip === 'none' ) {
1014 self::setPreserveKeysList( $item, [ $key ] );
1015 }
1016 } else {
1017 $item = [
1018 $key => $k,
1019 $valKey => $v,
1020 ];
1021 if ( $strip === 'none' ) {
1022 $item += [
1023 self::META_PRESERVE_KEYS => [ $key ],
1024 self::META_CONTENT => $valKey,
1025 self::META_TYPE => 'assoc',
1026 ];
1027 }
1028 }
1029 $ret[] = $assocAsObject ? (object)$item : $item;
1030 }
1031 $metadata[self::META_TYPE] = 'array';
1032
1033 // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
1034 return $ret + $keepMetadata;
1035
1036 default:
1037 throw new UnexpectedValueException( "Unknown type '$type'" );
1038 }
1039 }
1040
1051 public static function stripMetadata( $data ) {
1052 if ( is_array( $data ) || is_object( $data ) ) {
1053 $isObj = is_object( $data );
1054 if ( $isObj ) {
1055 $data = (array)$data;
1056 }
1057 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1058 ? (array)$data[self::META_PRESERVE_KEYS]
1059 : [];
1060 foreach ( $data as $k => $v ) {
1061 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1062 unset( $data[$k] );
1063 } elseif ( is_array( $v ) || is_object( $v ) ) {
1064 $data[$k] = self::stripMetadata( $v );
1065 }
1066 }
1067 if ( $isObj ) {
1068 $data = (object)$data;
1069 }
1070 }
1071 return $data;
1072 }
1073
1085 public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
1086 if ( !is_array( $metadata ) ) {
1087 $metadata = [];
1088 }
1089 if ( is_array( $data ) || is_object( $data ) ) {
1090 $isObj = is_object( $data );
1091 if ( $isObj ) {
1092 $data = (array)$data;
1093 }
1094 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1095 ? (array)$data[self::META_PRESERVE_KEYS]
1096 : [];
1097 foreach ( $data as $k => $v ) {
1098 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1099 $metadata[$k] = $v;
1100 unset( $data[$k] );
1101 }
1102 }
1103 if ( $isObj ) {
1104 $data = (object)$data;
1105 }
1106 }
1107 return $data;
1108 }
1109
1116 private static function size( $value ) {
1117 $s = 0;
1118 if ( is_array( $value ) ) {
1119 foreach ( $value as $k => $v ) {
1120 if ( !self::isMetadataKey( $k ) ) {
1121 $s += self::size( $v );
1122 }
1123 }
1124 } elseif ( is_scalar( $value ) ) {
1125 $s = strlen( $value );
1126 }
1127
1128 return $s;
1129 }
1130
1142 private function &path( $path, $create = 'append' ) {
1143 $path = (array)$path;
1144 $ret = &$this->data;
1145 foreach ( $path as $i => $k ) {
1146 if ( !isset( $ret[$k] ) ) {
1147 switch ( $create ) {
1148 case 'append':
1149 $ret[$k] = [];
1150 break;
1151 case 'prepend':
1152 $ret = [ $k => [] ] + $ret;
1153 break;
1154 case 'dummy':
1155 $tmp = [];
1156 return $tmp;
1157 default:
1158 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1159 throw new InvalidArgumentException( "Path $fail does not exist" );
1160 }
1161 }
1162 if ( !is_array( $ret[$k] ) ) {
1163 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1164 throw new InvalidArgumentException( "Path $fail is not an array" );
1165 }
1166 $ret = &$ret[$k];
1167 }
1168 return $ret;
1169 }
1170
1179 public static function addMetadataToResultVars( $vars, $forceHash = true ) {
1180 // Process subarrays and determine if this is a JS [] or {}
1181 $hash = $forceHash;
1182 $maxKey = -1;
1183 $bools = [];
1184 foreach ( $vars as $k => $v ) {
1185 if ( is_array( $v ) || is_object( $v ) ) {
1186 $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) );
1187 } elseif ( is_bool( $v ) ) {
1188 // Better here to use real bools even in BC formats
1189 $bools[] = $k;
1190 }
1191 if ( is_string( $k ) ) {
1192 $hash = true;
1193 } elseif ( $k > $maxKey ) {
1194 $maxKey = $k;
1195 }
1196 }
1197 if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
1198 $hash = true;
1199 }
1200
1201 // Set metadata appropriately
1202 if ( $hash ) {
1203 // Get the list of keys we actually care about. Unfortunately, we can't support
1204 // certain keys that conflict with ApiResult metadata.
1205 $keys = array_diff( array_keys( $vars ), [
1206 self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME,
1207 self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS
1208 ] );
1209
1210 return [
1211 self::META_TYPE => 'kvp',
1212 self::META_KVP_KEY_NAME => 'key',
1213 self::META_PRESERVE_KEYS => $keys,
1214 self::META_BC_BOOLS => $bools,
1215 self::META_INDEXED_TAG_NAME => 'var',
1216 ] + $vars;
1217 } else {
1218 return [
1219 self::META_TYPE => 'array',
1220 self::META_BC_BOOLS => $bools,
1221 self::META_INDEXED_TAG_NAME => 'value',
1222 ] + $vars;
1223 }
1224 }
1225
1234 public static function formatExpiry( $expiry, $infinity = 'infinity' ) {
1235 static $dbInfinity;
1236 $dbInfinity ??= MediaWikiServices::getInstance()->getConnectionProvider()
1237 ->getReplicaDatabase()
1238 ->getInfinity();
1239
1240 if ( $expiry === '' || $expiry === null || $expiry === false ||
1241 wfIsInfinity( $expiry ) || $expiry === $dbInfinity
1242 ) {
1243 return $infinity;
1244 } else {
1245 return wfTimestamp( TS_ISO_8601, $expiry );
1246 }
1247 }
1248
1249 // endregion -- end of Utility
1250
1251}
1252
1253/*
1254 * This file uses VisualStudio style region/endregion fold markers which are
1255 * recognised by PHPStorm. If modelines are enabled, the following editor
1256 * configuration will also enable folding in vim, if it is in the last 5 lines
1257 * of the file. We also use "@name" which creates sections in Doxygen.
1258 *
1259 * vim: foldmarker=//\ region,//\ endregion foldmethod=marker
1260 */
1261
1263class_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:45
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:51
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:59
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:68
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:94
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:76
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:88
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:82
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.
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:157
Value object representing a message parameter that consists of a list of values.
Definition ListParam.php:15
Value object representing a message parameter holding a single value.
This interface allows for overriding the default conversion applied by ApiResult::validateValue().