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 ( is_callable( [ $value, '__toString' ] ) ) {
359 $value = (string)$value;
360 } else {
361 $value = (array)$value + [ self::META_TYPE => 'assoc' ];
362 }
363 }
364
365 if ( is_string( $value ) ) {
366 // Optimization: avoid querying the service locator for each value.
367 static $contentLanguage = null;
368 if ( !$contentLanguage ) {
369 $contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
370 }
371 $value = $contentLanguage->normalize( $value );
372 } elseif ( is_array( $value ) ) {
373 foreach ( $value as $k => $v ) {
374 $value[$k] = self::validateValue( $v );
375 }
376 } elseif ( $value !== null && !is_scalar( $value ) ) {
377 $type = get_debug_type( $value );
378 throw new InvalidArgumentException( "Cannot add $type to ApiResult" );
379 } elseif ( is_float( $value ) && !is_finite( $value ) ) {
380 throw new InvalidArgumentException( 'Cannot add non-finite floats to ApiResult' );
381 }
382
383 return $value;
384 }
385
402 public function addValue( $path, $name, $value, $flags = 0 ) {
403 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
404
405 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
406 // self::size needs the validated value. Then flag
407 // to not re-validate later.
408 $value = self::validateValue( $value );
409 $flags |= self::NO_VALIDATE;
410
411 $newsize = $this->size + self::size( $value );
412 if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
413 $this->errorFormatter->addWarning(
414 'result', [ 'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ]
415 );
416 return false;
417 }
418 $this->size = $newsize;
419 }
420
421 self::setValue( $arr, $name, $value, $flags );
422 return true;
423 }
424
431 public static function unsetValue( array &$arr, $name ) {
432 $ret = null;
433 if ( isset( $arr[$name] ) ) {
434 $ret = $arr[$name];
435 unset( $arr[$name] );
436 }
437 return $ret;
438 }
439
450 public function removeValue( $path, $name, $flags = 0 ) {
451 $path = (array)$path;
452 if ( $name === null ) {
453 if ( !$path ) {
454 throw new InvalidArgumentException( 'Cannot remove the data root' );
455 }
456 $name = array_pop( $path );
457 }
458 $ret = self::unsetValue( $this->path( $path, 'dummy' ), $name );
459 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
460 $newsize = $this->size - self::size( $ret );
461 $this->size = max( $newsize, 0 );
462 }
463 return $ret;
464 }
465
475 public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) {
476 if ( $name === null ) {
477 throw new InvalidArgumentException( 'Content value must be named' );
478 }
479 self::setContentField( $arr, $name, $flags );
480 self::setValue( $arr, $name, $value, $flags );
481 }
482
493 public function addContentValue( $path, $name, $value, $flags = 0 ) {
494 if ( $name === null ) {
495 throw new InvalidArgumentException( 'Content value must be named' );
496 }
497 $this->addContentField( $path, $name, $flags );
498 return $this->addValue( $path, $name, $value, $flags );
499 }
500
508 public function addParsedLimit( $moduleName, $limit ) {
509 // Add value, allowing overwriting
510 $this->addValue( 'limits', $moduleName, $limit,
511 self::OVERRIDE | self::NO_SIZE_CHECK );
512 }
513
514 // endregion -- end of Content
515
516 /***************************************************************************/
517 // region Metadata
528 public static function setContentField( array &$arr, $name, $flags = 0 ) {
529 if ( isset( $arr[self::META_CONTENT] ) &&
530 isset( $arr[$arr[self::META_CONTENT]] ) &&
531 !( $flags & self::OVERRIDE )
532 ) {
533 throw new RuntimeException(
534 "Attempting to set content element as $name when " . $arr[self::META_CONTENT] .
535 ' is already set as the content element'
536 );
537 }
538 $arr[self::META_CONTENT] = $name;
539 }
540
549 public function addContentField( $path, $name, $flags = 0 ) {
550 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
551 self::setContentField( $arr, $name, $flags );
552 }
553
561 public static function setSubelementsList( array &$arr, $names ) {
562 if ( !isset( $arr[self::META_SUBELEMENTS] ) ) {
563 $arr[self::META_SUBELEMENTS] = (array)$names;
564 } else {
565 $arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names );
566 }
567 }
568
576 public function addSubelementsList( $path, $names ) {
577 $arr = &$this->path( $path );
578 self::setSubelementsList( $arr, $names );
579 }
580
588 public static function unsetSubelementsList( array &$arr, $names ) {
589 if ( isset( $arr[self::META_SUBELEMENTS] ) ) {
590 $arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names );
591 }
592 }
593
601 public function removeSubelementsList( $path, $names ) {
602 $arr = &$this->path( $path );
603 self::unsetSubelementsList( $arr, $names );
604 }
605
612 public static function setIndexedTagName( array &$arr, $tag ) {
613 if ( !is_string( $tag ) ) {
614 throw new InvalidArgumentException( 'Bad tag name' );
615 }
616 $arr[self::META_INDEXED_TAG_NAME] = $tag;
617 }
618
625 public function addIndexedTagName( $path, $tag ) {
626 $arr = &$this->path( $path );
627 self::setIndexedTagName( $arr, $tag );
628 }
629
637 public static function setIndexedTagNameRecursive( array &$arr, $tag ) {
638 if ( !is_string( $tag ) ) {
639 throw new InvalidArgumentException( 'Bad tag name' );
640 }
641 $arr[self::META_INDEXED_TAG_NAME] = $tag;
642 foreach ( $arr as $k => &$v ) {
643 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
645 }
646 }
647 }
648
656 public function addIndexedTagNameRecursive( $path, $tag ) {
657 $arr = &$this->path( $path );
659 }
660
671 public static function setPreserveKeysList( array &$arr, $names ) {
672 if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) {
673 $arr[self::META_PRESERVE_KEYS] = (array)$names;
674 } else {
675 $arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names );
676 }
677 }
678
686 public function addPreserveKeysList( $path, $names ) {
687 $arr = &$this->path( $path );
688 self::setPreserveKeysList( $arr, $names );
689 }
690
698 public static function unsetPreserveKeysList( array &$arr, $names ) {
699 if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) {
700 $arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names );
701 }
702 }
703
711 public function removePreserveKeysList( $path, $names ) {
712 $arr = &$this->path( $path );
713 self::unsetPreserveKeysList( $arr, $names );
714 }
715
724 public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) {
725 if ( !in_array( $type, [
726 'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp'
727 ], true ) ) {
728 throw new InvalidArgumentException( 'Bad type' );
729 }
730 $arr[self::META_TYPE] = $type;
731 if ( is_string( $kvpKeyName ) ) {
732 $arr[self::META_KVP_KEY_NAME] = $kvpKeyName;
733 }
734 }
735
743 public function addArrayType( $path, $tag, $kvpKeyName = null ) {
744 $arr = &$this->path( $path );
745 self::setArrayType( $arr, $tag, $kvpKeyName );
746 }
747
755 public static function setArrayTypeRecursive( array &$arr, $type, $kvpKeyName = null ) {
756 self::setArrayType( $arr, $type, $kvpKeyName );
757 foreach ( $arr as $k => &$v ) {
758 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
759 self::setArrayTypeRecursive( $v, $type, $kvpKeyName );
760 }
761 }
762 }
763
771 public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) {
772 $arr = &$this->path( $path );
773 self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName );
774 }
775
776 // endregion -- end of Metadata
777
778 /***************************************************************************/
779 // region Utility
788 public static function isMetadataKey( $key ) {
789 // Optimization: This is a very hot and highly optimized code path. Note that ord() only
790 // considers the first character and also works with empty strings and integers.
791 // 95 corresponds to the '_' character.
792 return ord( $key ) === 95;
793 }
794
804 protected static function applyTransformations( array $dataIn, array $transforms ) {
805 $strip = $transforms['Strip'] ?? 'none';
806 if ( $strip === 'base' ) {
807 $transforms['Strip'] = 'none';
808 }
809 $transformTypes = $transforms['Types'] ?? null;
810 if ( $transformTypes !== null && !is_array( $transformTypes ) ) {
811 throw new InvalidArgumentException( __METHOD__ . ':Value for "Types" must be an array' );
812 }
813
814 $metadata = [];
815 $data = self::stripMetadataNonRecursive( $dataIn, $metadata );
816
817 if ( isset( $transforms['Custom'] ) ) {
818 if ( !is_callable( $transforms['Custom'] ) ) {
819 throw new InvalidArgumentException( __METHOD__ . ': Value for "Custom" must be callable' );
820 }
821 call_user_func_array( $transforms['Custom'], [ &$data, &$metadata ] );
822 }
823
824 if ( ( isset( $transforms['BC'] ) || $transformTypes !== null ) &&
825 isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] === 'BCkvp' &&
826 !isset( $metadata[self::META_KVP_KEY_NAME] )
827 ) {
828 throw new UnexpectedValueException( 'Type "BCkvp" used without setting ' .
829 'ApiResult::META_KVP_KEY_NAME metadata item' );
830 }
831
832 // BC transformations
833 $boolKeys = null;
834 if ( isset( $transforms['BC'] ) ) {
835 if ( !is_array( $transforms['BC'] ) ) {
836 throw new InvalidArgumentException( __METHOD__ . ':Value for "BC" must be an array' );
837 }
838 if ( !in_array( 'nobool', $transforms['BC'], true ) ) {
839 $boolKeys = isset( $metadata[self::META_BC_BOOLS] )
840 ? array_fill_keys( $metadata[self::META_BC_BOOLS], true )
841 : [];
842 }
843
844 if ( !in_array( 'no*', $transforms['BC'], true ) &&
845 isset( $metadata[self::META_CONTENT] ) && $metadata[self::META_CONTENT] !== '*'
846 ) {
847 $k = $metadata[self::META_CONTENT];
848 $data['*'] = $data[$k];
849 unset( $data[$k] );
850 $metadata[self::META_CONTENT] = '*';
851 }
852
853 if ( !in_array( 'nosub', $transforms['BC'], true ) &&
854 isset( $metadata[self::META_BC_SUBELEMENTS] )
855 ) {
856 foreach ( $metadata[self::META_BC_SUBELEMENTS] as $k ) {
857 if ( isset( $data[$k] ) ) {
858 $data[$k] = [
859 '*' => $data[$k],
860 self::META_CONTENT => '*',
861 self::META_TYPE => 'assoc',
862 ];
863 }
864 }
865 }
866
867 if ( isset( $metadata[self::META_TYPE] ) ) {
868 switch ( $metadata[self::META_TYPE] ) {
869 case 'BCarray':
870 case 'BCassoc':
871 $metadata[self::META_TYPE] = 'default';
872 break;
873 case 'BCkvp':
874 $transformTypes['ArmorKVP'] = $metadata[self::META_KVP_KEY_NAME];
875 break;
876 }
877 }
878 }
879
880 // Figure out type, do recursive calls, and do boolean transform if necessary
881 $defaultType = 'array';
882 $maxKey = -1;
883 foreach ( $data as $k => &$v ) {
884 $v = is_array( $v ) ? self::applyTransformations( $v, $transforms ) : $v;
885 if ( $boolKeys !== null && is_bool( $v ) && !isset( $boolKeys[$k] ) ) {
886 if ( !$v ) {
887 unset( $data[$k] );
888 continue;
889 }
890 $v = '';
891 }
892 if ( is_string( $k ) ) {
893 $defaultType = 'assoc';
894 } elseif ( $k > $maxKey ) {
895 $maxKey = $k;
896 }
897 }
898 unset( $v );
899
900 // Determine which metadata to keep
901 switch ( $strip ) {
902 case 'all':
903 case 'base':
904 $keepMetadata = [];
905 break;
906 case 'none':
907 $keepMetadata = &$metadata;
908 break;
909 case 'bc':
910 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal Type mismatch on pass-by-ref args
911 $keepMetadata = array_intersect_key( $metadata, [
912 self::META_INDEXED_TAG_NAME => 1,
913 self::META_SUBELEMENTS => 1,
914 ] );
915 break;
916 default:
917 throw new InvalidArgumentException( __METHOD__ . ': Unknown value for "Strip"' );
918 }
919
920 // No type transformation
921 if ( $transformTypes === null ) {
922 return $data + $keepMetadata;
923 }
924
925 if ( $defaultType === 'array' && $maxKey !== count( $data ) - 1 ) {
926 $defaultType = 'assoc';
927 }
928
929 // Override type, if provided
930 $type = $defaultType;
931 if ( isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] !== 'default' ) {
932 $type = $metadata[self::META_TYPE];
933 }
934 if ( ( $type === 'kvp' || $type === 'BCkvp' ) &&
935 empty( $transformTypes['ArmorKVP'] )
936 ) {
937 $type = 'assoc';
938 } elseif ( $type === 'BCarray' ) {
939 $type = 'array';
940 } elseif ( $type === 'BCassoc' ) {
941 $type = 'assoc';
942 }
943
944 // Apply transformation
945 switch ( $type ) {
946 case 'assoc':
947 $metadata[self::META_TYPE] = 'assoc';
948 $data += $keepMetadata;
949 return empty( $transformTypes['AssocAsObject'] ) ? $data : (object)$data;
950
951 case 'array':
952 // Sort items in ascending order by key. Note that $data may contain a mix of number and string keys,
953 // for which the sorting behavior of krsort() with SORT_REGULAR is inconsistent between PHP versions.
954 // Given a comparison of a string key and a number key, PHP < 8.2 coerces the string key into a number
955 // (which yields zero if the string was non-numeric), and then performs the comparison,
956 // while PHP >= 8.2 makes the behavior consistent with stricter numeric comparisons introduced by
957 // PHP 8.0 in that if the string key is non-numeric, it converts the number key into a string
958 // and compares those two strings instead. We therefore use a custom comparison function
959 // implementing PHP >= 8.2 ordering semantics to ensure consistent ordering of items
960 // irrespective of the PHP version (T326480).
961 uksort( $data, static function ( $a, $b ): int {
962 // In a comparison of a number or numeric string with a non-numeric string,
963 // coerce both values into a string prior to comparing and compare the resulting strings.
964 // Note that PHP prior to 8.0 did not consider numeric strings with trailing whitespace
965 // to be numeric, so trim the inputs prior to the numeric checks to make the behavior
966 // consistent across PHP versions.
967 if ( is_numeric( trim( $a ) ) xor is_numeric( trim( $b ) ) ) {
968 return (string)$a <=> (string)$b;
969 }
970
971 return $a <=> $b;
972 } );
973
974 $data = array_values( $data );
975 $metadata[self::META_TYPE] = 'array';
976 // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
977 return $data + $keepMetadata;
978
979 case 'kvp':
980 case 'BCkvp':
981 $key = $metadata[self::META_KVP_KEY_NAME] ?? $transformTypes['ArmorKVP'];
982 $valKey = isset( $transforms['BC'] ) ? '*' : 'value';
983 $assocAsObject = !empty( $transformTypes['AssocAsObject'] );
984 $merge = !empty( $metadata[self::META_KVP_MERGE] );
985
986 $ret = [];
987 foreach ( $data as $k => $v ) {
988 if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) {
989 $vArr = (array)$v;
990 if ( isset( $vArr[self::META_TYPE] ) ) {
991 $mergeType = $vArr[self::META_TYPE];
992 } elseif ( is_object( $v ) ) {
993 $mergeType = 'assoc';
994 } else {
995 $keys = array_keys( $vArr );
996 sort( $keys, SORT_NUMERIC );
997 $mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc';
998 }
999 } else {
1000 $mergeType = 'n/a';
1001 }
1002 if ( $mergeType === 'assoc' ) {
1003 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable vArr set when used
1004 $item = $vArr + [
1005 $key => $k,
1006 ];
1007 if ( $strip === 'none' ) {
1008 self::setPreserveKeysList( $item, [ $key ] );
1009 }
1010 } else {
1011 $item = [
1012 $key => $k,
1013 $valKey => $v,
1014 ];
1015 if ( $strip === 'none' ) {
1016 $item += [
1017 self::META_PRESERVE_KEYS => [ $key ],
1018 self::META_CONTENT => $valKey,
1019 self::META_TYPE => 'assoc',
1020 ];
1021 }
1022 }
1023 $ret[] = $assocAsObject ? (object)$item : $item;
1024 }
1025 $metadata[self::META_TYPE] = 'array';
1026
1027 // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
1028 return $ret + $keepMetadata;
1029
1030 default:
1031 throw new UnexpectedValueException( "Unknown type '$type'" );
1032 }
1033 }
1034
1045 public static function stripMetadata( $data ) {
1046 if ( is_array( $data ) || is_object( $data ) ) {
1047 $isObj = is_object( $data );
1048 if ( $isObj ) {
1049 $data = (array)$data;
1050 }
1051 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1052 ? (array)$data[self::META_PRESERVE_KEYS]
1053 : [];
1054 foreach ( $data as $k => $v ) {
1055 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1056 unset( $data[$k] );
1057 } elseif ( is_array( $v ) || is_object( $v ) ) {
1058 $data[$k] = self::stripMetadata( $v );
1059 }
1060 }
1061 if ( $isObj ) {
1062 $data = (object)$data;
1063 }
1064 }
1065 return $data;
1066 }
1067
1079 public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
1080 if ( !is_array( $metadata ) ) {
1081 $metadata = [];
1082 }
1083 if ( is_array( $data ) || is_object( $data ) ) {
1084 $isObj = is_object( $data );
1085 if ( $isObj ) {
1086 $data = (array)$data;
1087 }
1088 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1089 ? (array)$data[self::META_PRESERVE_KEYS]
1090 : [];
1091 foreach ( $data as $k => $v ) {
1092 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1093 $metadata[$k] = $v;
1094 unset( $data[$k] );
1095 }
1096 }
1097 if ( $isObj ) {
1098 $data = (object)$data;
1099 }
1100 }
1101 return $data;
1102 }
1103
1110 private static function size( $value ) {
1111 $s = 0;
1112 if ( is_array( $value ) ) {
1113 foreach ( $value as $k => $v ) {
1114 if ( !self::isMetadataKey( $k ) ) {
1115 $s += self::size( $v );
1116 }
1117 }
1118 } elseif ( is_scalar( $value ) ) {
1119 $s = strlen( $value );
1120 }
1121
1122 return $s;
1123 }
1124
1136 private function &path( $path, $create = 'append' ) {
1137 $path = (array)$path;
1138 $ret = &$this->data;
1139 foreach ( $path as $i => $k ) {
1140 if ( !isset( $ret[$k] ) ) {
1141 switch ( $create ) {
1142 case 'append':
1143 $ret[$k] = [];
1144 break;
1145 case 'prepend':
1146 $ret = [ $k => [] ] + $ret;
1147 break;
1148 case 'dummy':
1149 $tmp = [];
1150 return $tmp;
1151 default:
1152 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1153 throw new InvalidArgumentException( "Path $fail does not exist" );
1154 }
1155 }
1156 if ( !is_array( $ret[$k] ) ) {
1157 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1158 throw new InvalidArgumentException( "Path $fail is not an array" );
1159 }
1160 $ret = &$ret[$k];
1161 }
1162 return $ret;
1163 }
1164
1173 public static function addMetadataToResultVars( $vars, $forceHash = true ) {
1174 // Process subarrays and determine if this is a JS [] or {}
1175 $hash = $forceHash;
1176 $maxKey = -1;
1177 $bools = [];
1178 foreach ( $vars as $k => $v ) {
1179 if ( is_array( $v ) || is_object( $v ) ) {
1180 $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) );
1181 } elseif ( is_bool( $v ) ) {
1182 // Better here to use real bools even in BC formats
1183 $bools[] = $k;
1184 }
1185 if ( is_string( $k ) ) {
1186 $hash = true;
1187 } elseif ( $k > $maxKey ) {
1188 $maxKey = $k;
1189 }
1190 }
1191 if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
1192 $hash = true;
1193 }
1194
1195 // Set metadata appropriately
1196 if ( $hash ) {
1197 // Get the list of keys we actually care about. Unfortunately, we can't support
1198 // certain keys that conflict with ApiResult metadata.
1199 $keys = array_diff( array_keys( $vars ), [
1200 self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME,
1201 self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS
1202 ] );
1203
1204 return [
1205 self::META_TYPE => 'kvp',
1206 self::META_KVP_KEY_NAME => 'key',
1207 self::META_PRESERVE_KEYS => $keys,
1208 self::META_BC_BOOLS => $bools,
1209 self::META_INDEXED_TAG_NAME => 'var',
1210 ] + $vars;
1211 } else {
1212 return [
1213 self::META_TYPE => 'array',
1214 self::META_BC_BOOLS => $bools,
1215 self::META_INDEXED_TAG_NAME => 'value',
1216 ] + $vars;
1217 }
1218 }
1219
1228 public static function formatExpiry( $expiry, $infinity = 'infinity' ) {
1229 static $dbInfinity;
1230 $dbInfinity ??= MediaWikiServices::getInstance()->getConnectionProvider()
1231 ->getReplicaDatabase()
1232 ->getInfinity();
1233
1234 if ( $expiry === '' || $expiry === null || $expiry === false ||
1235 wfIsInfinity( $expiry ) || $expiry === $dbInfinity
1236 ) {
1237 return $infinity;
1238 } else {
1239 return wfTimestamp( TS_ISO_8601, $expiry );
1240 }
1241 }
1242
1243 // endregion -- end of Utility
1244
1245}
1246
1247/*
1248 * This file uses VisualStudio style region/endregion fold markers which are
1249 * recognised by PHPStorm. If modelines are enabled, the following editor
1250 * configuration will also enable folding in vim, if it is in the last 5 lines
1251 * of the file. We also use "@name" which creates sections in Doxygen.
1252 *
1253 * vim: foldmarker=//\ region,//\ endregion foldmethod=marker
1254 */
1255
1257class_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:150
This interface allows for overriding the default conversion applied by ApiResult::validateValue().