MediaWiki master
ApiResult.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Api;
8
9use Exception;
10use InvalidArgumentException;
13use RuntimeException;
14use stdClass;
15use UnexpectedValueException;
20use Wikimedia\Timestamp\TimestampFormat as TS;
21
34class ApiResult implements ApiSerializable {
35
40 public const OVERRIDE = 1;
41
48 public const ADD_ON_TOP = 2;
49
57 public const NO_SIZE_CHECK = 4;
58
65 public const NO_VALIDATE = self::NO_SIZE_CHECK | 8;
66
72 public const IGNORE_CONFLICT_KEYS = 16;
73
78 public const META_INDEXED_TAG_NAME = '_element';
79
84 public const META_SUBELEMENTS = '_subelements';
85
90 public const META_PRESERVE_KEYS = '_preservekeys';
91
96 public const META_CONTENT = '_content';
97
116 public const META_TYPE = '_type';
117
125 public const META_KVP_KEY_NAME = '_kvpkeyname';
126
135 public const META_KVP_MERGE = '_kvpmerge';
136
142 public const META_BC_BOOLS = '_BC_bools';
143
149 public const META_BC_SUBELEMENTS = '_BC_subelements';
150
152 private $data;
153 private int $size;
155 private $maxSize;
156 private ApiErrorFormatter $errorFormatter;
157
161 public function __construct( $maxSize ) {
162 $this->maxSize = $maxSize;
163 $this->reset();
164 }
165
170 public function setErrorFormatter( ApiErrorFormatter $formatter ) {
171 $this->errorFormatter = $formatter;
172 }
173
179 public function serializeForApiResult() {
180 return $this->data;
181 }
182
183 /***************************************************************************/
184 // region Content
190 public function reset() {
191 $this->data = [
192 self::META_TYPE => 'assoc', // Usually what's desired
193 ];
194 $this->size = 0;
195 }
196
250 public function getResultData( $path = [], $transforms = [] ) {
251 $path = (array)$path;
252 if ( !$path ) {
253 return self::applyTransformations( $this->data, $transforms );
254 }
255
256 $last = array_pop( $path );
257 $ret = &$this->path( $path, 'dummy' );
258 if ( !isset( $ret[$last] ) ) {
259 return null;
260 } elseif ( is_array( $ret[$last] ) ) {
261 return self::applyTransformations( $ret[$last], $transforms );
262 } else {
263 return $ret[$last];
264 }
265 }
266
271 public function getSize() {
272 return $this->size;
273 }
274
287 public static function setValue( array &$arr, $name, $value, $flags = 0 ) {
288 if ( ( $flags & self::NO_VALIDATE ) !== self::NO_VALIDATE ) {
289 $value = self::validateValue( $value );
290 }
291
292 if ( $name === null ) {
293 if ( $flags & self::ADD_ON_TOP ) {
294 array_unshift( $arr, $value );
295 } else {
296 $arr[] = $value;
297 }
298 return;
299 }
300
301 $exists = isset( $arr[$name] );
302 if ( !$exists || ( $flags & self::OVERRIDE ) ) {
303 if ( !$exists && ( $flags & self::ADD_ON_TOP ) ) {
304 $arr = [ $name => $value ] + $arr;
305 } else {
306 $arr[$name] = $value;
307 }
308 } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
309 $conflicts = array_intersect_key( $arr[$name], $value );
310 if ( !$conflicts || ( $flags & self::IGNORE_CONFLICT_KEYS ) ) {
311 $arr[$name] += $value;
312 } else {
313 $keys = implode( ', ', array_keys( $conflicts ) );
314 throw new RuntimeException(
315 "Conflicting keys ($keys) when attempting to merge element $name"
316 );
317 }
318 } elseif ( $value !== $arr[$name] ) {
319 throw new RuntimeException(
320 "Attempting to add element $name=$value, existing value is {$arr[$name]}"
321 );
322 }
323 }
324
330 private static function validateValue( $value ) {
331 if ( is_object( $value ) ) {
332 // Note we use is_callable() here instead of instanceof because
333 // ApiSerializable is an informal protocol (see docs there for details).
334 if ( is_callable( [ $value, 'serializeForApiResult' ] ) ) {
335 $oldValue = $value;
336 $value = $value->serializeForApiResult();
337 if ( is_object( $value ) ) {
338 throw new UnexpectedValueException(
339 get_class( $oldValue ) . '::serializeForApiResult() returned an object of class ' .
340 get_class( $value )
341 );
342 }
343
344 // Recursive call instead of fall-through so we can throw a
345 // better exception message.
346 try {
347 return self::validateValue( $value );
348 } catch ( Exception $ex ) {
349 throw new UnexpectedValueException(
350 get_class( $oldValue ) . '::serializeForApiResult() returned an invalid value: ' .
351 $ex->getMessage(),
352 0,
353 $ex
354 );
355 }
356 } elseif ( $value instanceof ScalarParam || $value instanceof ListParam ) {
357 // HACK Support code that puts $msg->getParams() directly into API responses
358 // (e.g. ApiErrorFormatter::formatRawMessage()).
359 $value = $value->getType() === ParamType::TEXT ? $value->getValue() : $value->toJsonArray();
360 if ( $value instanceof MessageValue ) {
361 $value = $value->toJsonArray();
362 }
363 } elseif ( $value instanceof MessageValue ) {
364 // HACK Support code that puts $msg->getParams() directly into API responses
365 // (e.g. ApiErrorFormatter::formatRawMessage()).
366 $value = $value->toJsonArray();
367 } elseif ( is_callable( [ $value, '__toString' ] ) ) {
368 $value = (string)$value;
369 } else {
370 $value = (array)$value + [ self::META_TYPE => 'assoc' ];
371 }
372 }
373
374 if ( is_string( $value ) ) {
375 // Optimization: avoid querying the service locator for each value.
376 static $contentLanguage = null;
377 if ( !$contentLanguage ) {
378 $contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
379 }
380 $value = $contentLanguage->normalize( $value );
381 } elseif ( is_array( $value ) ) {
382 foreach ( $value as $k => $v ) {
383 $value[$k] = self::validateValue( $v );
384 }
385 } elseif ( $value !== null && !is_scalar( $value ) ) {
386 $type = get_debug_type( $value );
387 throw new InvalidArgumentException( "Cannot add $type to ApiResult" );
388 } elseif ( is_float( $value ) && !is_finite( $value ) ) {
389 throw new InvalidArgumentException( 'Cannot add non-finite floats to ApiResult' );
390 }
391
392 return $value;
393 }
394
411 public function addValue( $path, $name, $value, $flags = 0 ) {
412 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
413
414 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
415 // self::size needs the validated value. Then flag
416 // to not re-validate later.
417 $value = self::validateValue( $value );
418 $flags |= self::NO_VALIDATE;
419
420 $newsize = $this->size + self::size( $value );
421 if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
422 $this->errorFormatter->addWarning(
423 'result', [ 'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ]
424 );
425 return false;
426 }
427 $this->size = $newsize;
428 }
429
430 self::setValue( $arr, $name, $value, $flags );
431 return true;
432 }
433
440 public static function unsetValue( array &$arr, $name ) {
441 $ret = null;
442 if ( isset( $arr[$name] ) ) {
443 $ret = $arr[$name];
444 unset( $arr[$name] );
445 }
446 return $ret;
447 }
448
459 public function removeValue( $path, $name, $flags = 0 ) {
460 $path = (array)$path;
461 if ( $name === null ) {
462 if ( !$path ) {
463 throw new InvalidArgumentException( 'Cannot remove the data root' );
464 }
465 $name = array_pop( $path );
466 }
467 $ret = self::unsetValue( $this->path( $path, 'dummy' ), $name );
468 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
469 $newsize = $this->size - self::size( $ret );
470 $this->size = max( $newsize, 0 );
471 }
472 return $ret;
473 }
474
484 public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) {
485 if ( $name === null ) {
486 throw new InvalidArgumentException( 'Content value must be named' );
487 }
488 self::setContentField( $arr, $name, $flags );
489 self::setValue( $arr, $name, $value, $flags );
490 }
491
502 public function addContentValue( $path, $name, $value, $flags = 0 ) {
503 if ( $name === null ) {
504 throw new InvalidArgumentException( 'Content value must be named' );
505 }
506 $this->addContentField( $path, $name, $flags );
507 return $this->addValue( $path, $name, $value, $flags );
508 }
509
517 public function addParsedLimit( $moduleName, $limit ) {
518 // Add value, allowing overwriting
519 $this->addValue( 'limits', $moduleName, $limit,
520 self::OVERRIDE | self::NO_SIZE_CHECK );
521 }
522
523 // endregion -- end of Content
524
525 /***************************************************************************/
526 // region Metadata
537 public static function setContentField( array &$arr, $name, $flags = 0 ) {
538 if ( isset( $arr[self::META_CONTENT] ) &&
539 isset( $arr[$arr[self::META_CONTENT]] ) &&
540 !( $flags & self::OVERRIDE )
541 ) {
542 throw new RuntimeException(
543 "Attempting to set content element as $name when " . $arr[self::META_CONTENT] .
544 ' is already set as the content element'
545 );
546 }
547 $arr[self::META_CONTENT] = $name;
548 }
549
558 public function addContentField( $path, $name, $flags = 0 ) {
559 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
560 self::setContentField( $arr, $name, $flags );
561 }
562
570 public static function setSubelementsList( array &$arr, $names ) {
571 if ( !isset( $arr[self::META_SUBELEMENTS] ) ) {
572 $arr[self::META_SUBELEMENTS] = (array)$names;
573 } else {
574 $arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names );
575 }
576 }
577
585 public function addSubelementsList( $path, $names ) {
586 $arr = &$this->path( $path );
587 self::setSubelementsList( $arr, $names );
588 }
589
597 public static function unsetSubelementsList( array &$arr, $names ) {
598 if ( isset( $arr[self::META_SUBELEMENTS] ) ) {
599 $arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names );
600 }
601 }
602
610 public function removeSubelementsList( $path, $names ) {
611 $arr = &$this->path( $path );
612 self::unsetSubelementsList( $arr, $names );
613 }
614
621 public static function setIndexedTagName( array &$arr, $tag ) {
622 if ( !is_string( $tag ) ) {
623 throw new InvalidArgumentException( 'Bad tag name' );
624 }
625 $arr[self::META_INDEXED_TAG_NAME] = $tag;
626 }
627
634 public function addIndexedTagName( $path, $tag ) {
635 $arr = &$this->path( $path );
636 self::setIndexedTagName( $arr, $tag );
637 }
638
646 public static function setIndexedTagNameRecursive( array &$arr, $tag ) {
647 if ( !is_string( $tag ) ) {
648 throw new InvalidArgumentException( 'Bad tag name' );
649 }
650 $arr[self::META_INDEXED_TAG_NAME] = $tag;
651 foreach ( $arr as $k => &$v ) {
652 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
654 }
655 }
656 }
657
665 public function addIndexedTagNameRecursive( $path, $tag ) {
666 $arr = &$this->path( $path );
668 }
669
680 public static function setPreserveKeysList( array &$arr, $names ) {
681 if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) {
682 $arr[self::META_PRESERVE_KEYS] = (array)$names;
683 } else {
684 $arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names );
685 }
686 }
687
695 public function addPreserveKeysList( $path, $names ) {
696 $arr = &$this->path( $path );
697 self::setPreserveKeysList( $arr, $names );
698 }
699
707 public static function unsetPreserveKeysList( array &$arr, $names ) {
708 if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) {
709 $arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names );
710 }
711 }
712
720 public function removePreserveKeysList( $path, $names ) {
721 $arr = &$this->path( $path );
722 self::unsetPreserveKeysList( $arr, $names );
723 }
724
733 public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) {
734 if ( !in_array( $type, [
735 'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp'
736 ], true ) ) {
737 throw new InvalidArgumentException( 'Bad type' );
738 }
739 $arr[self::META_TYPE] = $type;
740 if ( is_string( $kvpKeyName ) ) {
741 $arr[self::META_KVP_KEY_NAME] = $kvpKeyName;
742 }
743 }
744
752 public function addArrayType( $path, $tag, $kvpKeyName = null ) {
753 $arr = &$this->path( $path );
754 self::setArrayType( $arr, $tag, $kvpKeyName );
755 }
756
764 public static function setArrayTypeRecursive( array &$arr, $type, $kvpKeyName = null ) {
765 self::setArrayType( $arr, $type, $kvpKeyName );
766 foreach ( $arr as $k => &$v ) {
767 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
768 self::setArrayTypeRecursive( $v, $type, $kvpKeyName );
769 }
770 }
771 }
772
780 public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) {
781 $arr = &$this->path( $path );
782 self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName );
783 }
784
785 // endregion -- end of Metadata
786
787 /***************************************************************************/
788 // region Utility
797 public static function isMetadataKey( $key ) {
798 return str_starts_with( $key, '_' );
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 if ( is_numeric( $a ) xor is_numeric( $b ) ) {
971 return (string)$a <=> (string)$b;
972 }
973
974 return $a <=> $b;
975 } );
976
977 $data = array_values( $data );
978 $metadata[self::META_TYPE] = 'array';
979 return $data + $keepMetadata;
980
981 case 'kvp':
982 case 'BCkvp':
983 $key = $metadata[self::META_KVP_KEY_NAME] ?? $transformTypes['ArmorKVP'];
984 $valKey = isset( $transforms['BC'] ) ? '*' : 'value';
985 $assocAsObject = !empty( $transformTypes['AssocAsObject'] );
986 $merge = !empty( $metadata[self::META_KVP_MERGE] );
987
988 $ret = [];
989 foreach ( $data as $k => $v ) {
990 if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) {
991 $vArr = (array)$v;
992 if ( isset( $vArr[self::META_TYPE] ) ) {
993 $mergeType = $vArr[self::META_TYPE];
994 } elseif ( is_object( $v ) ) {
995 $mergeType = 'assoc';
996 } else {
997 $keys = array_keys( $vArr );
998 sort( $keys, SORT_NUMERIC );
999 $mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc';
1000 }
1001 } else {
1002 $mergeType = 'n/a';
1003 }
1004 if ( $mergeType === 'assoc' ) {
1005 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable vArr set when used
1006 $item = $vArr + [
1007 $key => $k,
1008 ];
1009 if ( $strip === 'none' ) {
1010 self::setPreserveKeysList( $item, [ $key ] );
1011 }
1012 } else {
1013 $item = [
1014 $key => $k,
1015 $valKey => $v,
1016 ];
1017 if ( $strip === 'none' ) {
1018 $item += [
1019 self::META_PRESERVE_KEYS => [ $key ],
1020 self::META_CONTENT => $valKey,
1021 self::META_TYPE => 'assoc',
1022 ];
1023 }
1024 }
1025 $ret[] = $assocAsObject ? (object)$item : $item;
1026 }
1027 $metadata[self::META_TYPE] = 'array';
1028
1029 return $ret + $keepMetadata;
1030
1031 default:
1032 throw new UnexpectedValueException( "Unknown type '$type'" );
1033 }
1034 }
1035
1046 public static function stripMetadata( $data ) {
1047 if ( is_array( $data ) || is_object( $data ) ) {
1048 $isObj = is_object( $data );
1049 if ( $isObj ) {
1050 $data = (array)$data;
1051 }
1052 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1053 ? (array)$data[self::META_PRESERVE_KEYS]
1054 : [];
1055 foreach ( $data as $k => $v ) {
1056 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1057 unset( $data[$k] );
1058 } elseif ( is_array( $v ) || is_object( $v ) ) {
1059 $data[$k] = self::stripMetadata( $v );
1060 }
1061 }
1062 if ( $isObj ) {
1063 $data = (object)$data;
1064 }
1065 }
1066 return $data;
1067 }
1068
1080 public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
1081 if ( !is_array( $metadata ) ) {
1082 $metadata = [];
1083 }
1084 if ( is_array( $data ) || is_object( $data ) ) {
1085 $isObj = is_object( $data );
1086 if ( $isObj ) {
1087 $data = (array)$data;
1088 }
1089 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1090 ? (array)$data[self::META_PRESERVE_KEYS]
1091 : [];
1092 foreach ( $data as $k => $v ) {
1093 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1094 $metadata[$k] = $v;
1095 unset( $data[$k] );
1096 }
1097 }
1098 if ( $isObj ) {
1099 $data = (object)$data;
1100 }
1101 }
1102 return $data;
1103 }
1104
1111 private static function size( $value ) {
1112 $s = 0;
1113 if ( is_array( $value ) ) {
1114 foreach ( $value as $k => $v ) {
1115 if ( !self::isMetadataKey( $k ) ) {
1116 $s += self::size( $v );
1117 }
1118 }
1119 } elseif ( is_scalar( $value ) ) {
1120 $s = strlen( $value );
1121 }
1122
1123 return $s;
1124 }
1125
1137 private function &path( $path, $create = 'append' ) {
1138 $path = (array)$path;
1139 $ret = &$this->data;
1140 foreach ( $path as $i => $k ) {
1141 if ( !isset( $ret[$k] ) ) {
1142 switch ( $create ) {
1143 case 'append':
1144 $ret[$k] = [];
1145 break;
1146 case 'prepend':
1147 $ret = [ $k => [] ] + $ret;
1148 break;
1149 case 'dummy':
1150 $tmp = [];
1151 return $tmp;
1152 default:
1153 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1154 throw new InvalidArgumentException( "Path $fail does not exist" );
1155 }
1156 }
1157 if ( !is_array( $ret[$k] ) ) {
1158 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1159 throw new InvalidArgumentException( "Path $fail is not an array" );
1160 }
1161 $ret = &$ret[$k];
1162 }
1163 return $ret;
1164 }
1165
1174 public static function addMetadataToResultVars( $vars, $forceHash = true ) {
1175 // Process subarrays and determine if this is a JS [] or {}
1176 $hash = $forceHash;
1177 $maxKey = -1;
1178 $bools = [];
1179 foreach ( $vars as $k => $v ) {
1180 if ( is_array( $v ) || is_object( $v ) ) {
1181 $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) );
1182 } elseif ( is_bool( $v ) ) {
1183 // Better here to use real bools even in BC formats
1184 $bools[] = $k;
1185 }
1186 if ( is_string( $k ) ) {
1187 $hash = true;
1188 } elseif ( $k > $maxKey ) {
1189 $maxKey = $k;
1190 }
1191 }
1192 if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
1193 $hash = true;
1194 }
1195
1196 // Set metadata appropriately
1197 if ( $hash ) {
1198 // Get the list of keys we actually care about. Unfortunately, we can't support
1199 // certain keys that conflict with ApiResult metadata.
1200 $keys = array_diff( array_keys( $vars ), [
1201 self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME,
1202 self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS
1203 ] );
1204
1205 return [
1206 self::META_TYPE => 'kvp',
1207 self::META_KVP_KEY_NAME => 'key',
1208 self::META_PRESERVE_KEYS => $keys,
1209 self::META_BC_BOOLS => $bools,
1210 self::META_INDEXED_TAG_NAME => 'var',
1211 ] + $vars;
1212 } else {
1213 return [
1214 self::META_TYPE => 'array',
1215 self::META_BC_BOOLS => $bools,
1216 self::META_INDEXED_TAG_NAME => 'value',
1217 ] + $vars;
1218 }
1219 }
1220
1229 public static function formatExpiry( $expiry, $infinity = 'infinity' ) {
1230 static $dbInfinity;
1231 $dbInfinity ??= MediaWikiServices::getInstance()->getConnectionProvider()
1232 ->getReplicaDatabase()
1233 ->getInfinity();
1234
1235 if ( $expiry === '' || $expiry === null || $expiry === false ||
1236 wfIsInfinity( $expiry ) || $expiry === $dbInfinity
1237 ) {
1238 return $infinity;
1239 } else {
1240 return wfTimestamp( TS::ISO_8601, $expiry );
1241 }
1242 }
1243
1244 // endregion -- end of Utility
1245
1246}
1247
1248/*
1249 * This file uses VisualStudio style region/endregion fold markers which are
1250 * recognised by PHPStorm. If modelines are enabled, the following editor
1251 * configuration will also enable folding in vim, if it is in the last 5 lines
1252 * of the file. We also use "@name" which creates sections in Doxygen.
1253 *
1254 * vim: foldmarker=//\ region,//\ endregion foldmethod=marker
1255 */
1256
1258class_alias( ApiResult::class, 'ApiResult' );
wfTimestamp( $outputtype=TS::UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfIsInfinity( $str)
Determine input string is represents as infinity.
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:34
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:40
removeValue( $path, $name, $flags=0)
Remove value from the output data at the given path.
const IGNORE_CONFLICT_KEYS
For addValue(), setValue() and similar functions, do allow override of conflicting keys.
Definition ApiResult.php:72
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:48
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:57
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:90
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:65
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:84
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:78
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:96
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:144
Value object representing a message parameter that consists of a list of values.
Definition ListParam.php:15
Value object representing a message for i18n.
Value object representing a message parameter holding a single value.
This interface allows for overriding the default conversion applied by ApiResult::validateValue().
ParamType
The constants used to specify parameter types.
Definition ParamType.php:11