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