MediaWiki master
ApiResult.php
Go to the documentation of this file.
1<?php
22
35class ApiResult implements ApiSerializable {
36
41 public const OVERRIDE = 1;
42
49 public const ADD_ON_TOP = 2;
50
58 public const NO_SIZE_CHECK = 4;
59
66 public const NO_VALIDATE = self::NO_SIZE_CHECK | 8;
67
72 public const META_INDEXED_TAG_NAME = '_element';
73
78 public const META_SUBELEMENTS = '_subelements';
79
84 public const META_PRESERVE_KEYS = '_preservekeys';
85
90 public const META_CONTENT = '_content';
91
110 public const META_TYPE = '_type';
111
119 public const META_KVP_KEY_NAME = '_kvpkeyname';
120
129 public const META_KVP_MERGE = '_kvpmerge';
130
136 public const META_BC_BOOLS = '_BC_bools';
137
143 public const META_BC_SUBELEMENTS = '_BC_subelements';
144
145 private $data, $size, $maxSize;
146 private $errorFormatter;
147
151 public function __construct( $maxSize ) {
152 $this->maxSize = $maxSize;
153 $this->reset();
154 }
155
160 public function setErrorFormatter( ApiErrorFormatter $formatter ) {
161 $this->errorFormatter = $formatter;
162 }
163
169 public function serializeForApiResult() {
170 return $this->data;
171 }
172
173 /***************************************************************************/
174 // region Content
180 public function reset() {
181 $this->data = [
182 self::META_TYPE => 'assoc', // Usually what's desired
183 ];
184 $this->size = 0;
185 }
186
240 public function getResultData( $path = [], $transforms = [] ) {
241 $path = (array)$path;
242 if ( !$path ) {
243 return self::applyTransformations( $this->data, $transforms );
244 }
245
246 $last = array_pop( $path );
247 $ret = &$this->path( $path, 'dummy' );
248 if ( !isset( $ret[$last] ) ) {
249 return null;
250 } elseif ( is_array( $ret[$last] ) ) {
251 return self::applyTransformations( $ret[$last], $transforms );
252 } else {
253 return $ret[$last];
254 }
255 }
256
261 public function getSize() {
262 return $this->size;
263 }
264
277 public static function setValue( array &$arr, $name, $value, $flags = 0 ) {
278 if ( ( $flags & self::NO_VALIDATE ) !== self::NO_VALIDATE ) {
279 $value = self::validateValue( $value );
280 }
281
282 if ( $name === null ) {
283 if ( $flags & self::ADD_ON_TOP ) {
284 array_unshift( $arr, $value );
285 } else {
286 $arr[] = $value;
287 }
288 return;
289 }
290
291 $exists = isset( $arr[$name] );
292 if ( !$exists || ( $flags & self::OVERRIDE ) ) {
293 if ( !$exists && ( $flags & self::ADD_ON_TOP ) ) {
294 $arr = [ $name => $value ] + $arr;
295 } else {
296 $arr[$name] = $value;
297 }
298 } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
299 $conflicts = array_intersect_key( $arr[$name], $value );
300 if ( !$conflicts ) {
301 $arr[$name] += $value;
302 } else {
303 $keys = implode( ', ', array_keys( $conflicts ) );
304 throw new RuntimeException(
305 "Conflicting keys ($keys) when attempting to merge element $name"
306 );
307 }
308 } elseif ( $value !== $arr[$name] ) {
309 throw new RuntimeException(
310 "Attempting to add element $name=$value, existing value is {$arr[$name]}"
311 );
312 }
313 }
314
320 private static function validateValue( $value ) {
321 if ( is_object( $value ) ) {
322 // Note we use is_callable() here instead of instanceof because
323 // ApiSerializable is an informal protocol (see docs there for details).
324 if ( is_callable( [ $value, 'serializeForApiResult' ] ) ) {
325 $oldValue = $value;
326 $value = $value->serializeForApiResult();
327 if ( is_object( $value ) ) {
328 throw new UnexpectedValueException(
329 get_class( $oldValue ) . '::serializeForApiResult() returned an object of class ' .
330 get_class( $value )
331 );
332 }
333
334 // Recursive call instead of fall-through so we can throw a
335 // better exception message.
336 try {
337 return self::validateValue( $value );
338 } catch ( Exception $ex ) {
339 throw new UnexpectedValueException(
340 get_class( $oldValue ) . '::serializeForApiResult() returned an invalid value: ' .
341 $ex->getMessage(),
342 0,
343 $ex
344 );
345 }
346 } elseif ( is_callable( [ $value, '__toString' ] ) ) {
347 $value = (string)$value;
348 } else {
349 $value = (array)$value + [ self::META_TYPE => 'assoc' ];
350 }
351 }
352
353 if ( is_string( $value ) ) {
354 // Optimization: avoid querying the service locator for each value.
355 static $contentLanguage = null;
356 if ( !$contentLanguage ) {
357 $contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
358 }
359 $value = $contentLanguage->normalize( $value );
360 } elseif ( is_array( $value ) ) {
361 foreach ( $value as $k => $v ) {
362 $value[$k] = self::validateValue( $v );
363 }
364 } elseif ( $value !== null && !is_scalar( $value ) ) {
365 $type = gettype( $value );
366 // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.is_resource
367 if ( is_resource( $value ) ) {
368 $type .= '(' . get_resource_type( $value ) . ')';
369 }
370 throw new InvalidArgumentException( "Cannot add $type to ApiResult" );
371 } elseif ( is_float( $value ) && !is_finite( $value ) ) {
372 throw new InvalidArgumentException( 'Cannot add non-finite floats to ApiResult' );
373 }
374
375 return $value;
376 }
377
394 public function addValue( $path, $name, $value, $flags = 0 ) {
395 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
396
397 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
398 // self::size needs the validated value. Then flag
399 // to not re-validate later.
400 $value = self::validateValue( $value );
401 $flags |= self::NO_VALIDATE;
402
403 $newsize = $this->size + self::size( $value );
404 if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
405 $this->errorFormatter->addWarning(
406 'result', [ 'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ]
407 );
408 return false;
409 }
410 $this->size = $newsize;
411 }
412
413 self::setValue( $arr, $name, $value, $flags );
414 return true;
415 }
416
423 public static function unsetValue( array &$arr, $name ) {
424 $ret = null;
425 if ( isset( $arr[$name] ) ) {
426 $ret = $arr[$name];
427 unset( $arr[$name] );
428 }
429 return $ret;
430 }
431
442 public function removeValue( $path, $name, $flags = 0 ) {
443 $path = (array)$path;
444 if ( $name === null ) {
445 if ( !$path ) {
446 throw new InvalidArgumentException( 'Cannot remove the data root' );
447 }
448 $name = array_pop( $path );
449 }
450 $ret = self::unsetValue( $this->path( $path, 'dummy' ), $name );
451 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
452 $newsize = $this->size - self::size( $ret );
453 $this->size = max( $newsize, 0 );
454 }
455 return $ret;
456 }
457
467 public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) {
468 if ( $name === null ) {
469 throw new InvalidArgumentException( 'Content value must be named' );
470 }
471 self::setContentField( $arr, $name, $flags );
472 self::setValue( $arr, $name, $value, $flags );
473 }
474
485 public function addContentValue( $path, $name, $value, $flags = 0 ) {
486 if ( $name === null ) {
487 throw new InvalidArgumentException( 'Content value must be named' );
488 }
489 $this->addContentField( $path, $name, $flags );
490 return $this->addValue( $path, $name, $value, $flags );
491 }
492
500 public function addParsedLimit( $moduleName, $limit ) {
501 // Add value, allowing overwriting
502 $this->addValue( 'limits', $moduleName, $limit,
503 self::OVERRIDE | self::NO_SIZE_CHECK );
504 }
505
506 // endregion -- end of Content
507
508 /***************************************************************************/
509 // region Metadata
520 public static function setContentField( array &$arr, $name, $flags = 0 ) {
521 if ( isset( $arr[self::META_CONTENT] ) &&
522 isset( $arr[$arr[self::META_CONTENT]] ) &&
523 !( $flags & self::OVERRIDE )
524 ) {
525 throw new RuntimeException(
526 "Attempting to set content element as $name when " . $arr[self::META_CONTENT] .
527 ' is already set as the content element'
528 );
529 }
530 $arr[self::META_CONTENT] = $name;
531 }
532
541 public function addContentField( $path, $name, $flags = 0 ) {
542 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
543 self::setContentField( $arr, $name, $flags );
544 }
545
553 public static function setSubelementsList( array &$arr, $names ) {
554 if ( !isset( $arr[self::META_SUBELEMENTS] ) ) {
555 $arr[self::META_SUBELEMENTS] = (array)$names;
556 } else {
557 $arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names );
558 }
559 }
560
568 public function addSubelementsList( $path, $names ) {
569 $arr = &$this->path( $path );
570 self::setSubelementsList( $arr, $names );
571 }
572
580 public static function unsetSubelementsList( array &$arr, $names ) {
581 if ( isset( $arr[self::META_SUBELEMENTS] ) ) {
582 $arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names );
583 }
584 }
585
593 public function removeSubelementsList( $path, $names ) {
594 $arr = &$this->path( $path );
595 self::unsetSubelementsList( $arr, $names );
596 }
597
604 public static function setIndexedTagName( array &$arr, $tag ) {
605 if ( !is_string( $tag ) ) {
606 throw new InvalidArgumentException( 'Bad tag name' );
607 }
608 $arr[self::META_INDEXED_TAG_NAME] = $tag;
609 }
610
617 public function addIndexedTagName( $path, $tag ) {
618 $arr = &$this->path( $path );
619 self::setIndexedTagName( $arr, $tag );
620 }
621
629 public static function setIndexedTagNameRecursive( array &$arr, $tag ) {
630 if ( !is_string( $tag ) ) {
631 throw new InvalidArgumentException( 'Bad tag name' );
632 }
633 $arr[self::META_INDEXED_TAG_NAME] = $tag;
634 foreach ( $arr as $k => &$v ) {
635 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
636 self::setIndexedTagNameRecursive( $v, $tag );
637 }
638 }
639 }
640
648 public function addIndexedTagNameRecursive( $path, $tag ) {
649 $arr = &$this->path( $path );
650 self::setIndexedTagNameRecursive( $arr, $tag );
651 }
652
663 public static function setPreserveKeysList( array &$arr, $names ) {
664 if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) {
665 $arr[self::META_PRESERVE_KEYS] = (array)$names;
666 } else {
667 $arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names );
668 }
669 }
670
678 public function addPreserveKeysList( $path, $names ) {
679 $arr = &$this->path( $path );
680 self::setPreserveKeysList( $arr, $names );
681 }
682
690 public static function unsetPreserveKeysList( array &$arr, $names ) {
691 if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) {
692 $arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names );
693 }
694 }
695
703 public function removePreserveKeysList( $path, $names ) {
704 $arr = &$this->path( $path );
705 self::unsetPreserveKeysList( $arr, $names );
706 }
707
716 public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) {
717 if ( !in_array( $type, [
718 'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp'
719 ], true ) ) {
720 throw new InvalidArgumentException( 'Bad type' );
721 }
722 $arr[self::META_TYPE] = $type;
723 if ( is_string( $kvpKeyName ) ) {
724 $arr[self::META_KVP_KEY_NAME] = $kvpKeyName;
725 }
726 }
727
735 public function addArrayType( $path, $tag, $kvpKeyName = null ) {
736 $arr = &$this->path( $path );
737 self::setArrayType( $arr, $tag, $kvpKeyName );
738 }
739
747 public static function setArrayTypeRecursive( array &$arr, $type, $kvpKeyName = null ) {
748 self::setArrayType( $arr, $type, $kvpKeyName );
749 foreach ( $arr as $k => &$v ) {
750 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
751 self::setArrayTypeRecursive( $v, $type, $kvpKeyName );
752 }
753 }
754 }
755
763 public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) {
764 $arr = &$this->path( $path );
765 self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName );
766 }
767
768 // endregion -- end of Metadata
769
770 /***************************************************************************/
771 // region Utility
780 public static function isMetadataKey( $key ) {
781 // Optimization: This is a very hot and highly optimized code path. Note that ord() only
782 // considers the first character and also works with empty strings and integers.
783 // 95 corresponds to the '_' character.
784 return ord( $key ) === 95;
785 }
786
796 protected static function applyTransformations( array $dataIn, array $transforms ) {
797 $strip = $transforms['Strip'] ?? 'none';
798 if ( $strip === 'base' ) {
799 $transforms['Strip'] = 'none';
800 }
801 $transformTypes = $transforms['Types'] ?? null;
802 if ( $transformTypes !== null && !is_array( $transformTypes ) ) {
803 throw new InvalidArgumentException( __METHOD__ . ':Value for "Types" must be an array' );
804 }
805
806 $metadata = [];
807 $data = self::stripMetadataNonRecursive( $dataIn, $metadata );
808
809 if ( isset( $transforms['Custom'] ) ) {
810 if ( !is_callable( $transforms['Custom'] ) ) {
811 throw new InvalidArgumentException( __METHOD__ . ': Value for "Custom" must be callable' );
812 }
813 call_user_func_array( $transforms['Custom'], [ &$data, &$metadata ] );
814 }
815
816 if ( ( isset( $transforms['BC'] ) || $transformTypes !== null ) &&
817 isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] === 'BCkvp' &&
818 !isset( $metadata[self::META_KVP_KEY_NAME] )
819 ) {
820 throw new UnexpectedValueException( 'Type "BCkvp" used without setting ' .
821 'ApiResult::META_KVP_KEY_NAME metadata item' );
822 }
823
824 // BC transformations
825 $boolKeys = null;
826 if ( isset( $transforms['BC'] ) ) {
827 if ( !is_array( $transforms['BC'] ) ) {
828 throw new InvalidArgumentException( __METHOD__ . ':Value for "BC" must be an array' );
829 }
830 if ( !in_array( 'nobool', $transforms['BC'], true ) ) {
831 $boolKeys = isset( $metadata[self::META_BC_BOOLS] )
832 ? array_fill_keys( $metadata[self::META_BC_BOOLS], true )
833 : [];
834 }
835
836 if ( !in_array( 'no*', $transforms['BC'], true ) &&
837 isset( $metadata[self::META_CONTENT] ) && $metadata[self::META_CONTENT] !== '*'
838 ) {
839 $k = $metadata[self::META_CONTENT];
840 $data['*'] = $data[$k];
841 unset( $data[$k] );
842 $metadata[self::META_CONTENT] = '*';
843 }
844
845 if ( !in_array( 'nosub', $transforms['BC'], true ) &&
846 isset( $metadata[self::META_BC_SUBELEMENTS] )
847 ) {
848 foreach ( $metadata[self::META_BC_SUBELEMENTS] as $k ) {
849 if ( isset( $data[$k] ) ) {
850 $data[$k] = [
851 '*' => $data[$k],
852 self::META_CONTENT => '*',
853 self::META_TYPE => 'assoc',
854 ];
855 }
856 }
857 }
858
859 if ( isset( $metadata[self::META_TYPE] ) ) {
860 switch ( $metadata[self::META_TYPE] ) {
861 case 'BCarray':
862 case 'BCassoc':
863 $metadata[self::META_TYPE] = 'default';
864 break;
865 case 'BCkvp':
866 $transformTypes['ArmorKVP'] = $metadata[self::META_KVP_KEY_NAME];
867 break;
868 }
869 }
870 }
871
872 // Figure out type, do recursive calls, and do boolean transform if necessary
873 $defaultType = 'array';
874 $maxKey = -1;
875 foreach ( $data as $k => &$v ) {
876 $v = is_array( $v ) ? self::applyTransformations( $v, $transforms ) : $v;
877 if ( $boolKeys !== null && is_bool( $v ) && !isset( $boolKeys[$k] ) ) {
878 if ( !$v ) {
879 unset( $data[$k] );
880 continue;
881 }
882 $v = '';
883 }
884 if ( is_string( $k ) ) {
885 $defaultType = 'assoc';
886 } elseif ( $k > $maxKey ) {
887 $maxKey = $k;
888 }
889 }
890 unset( $v );
891
892 // Determine which metadata to keep
893 switch ( $strip ) {
894 case 'all':
895 case 'base':
896 $keepMetadata = [];
897 break;
898 case 'none':
899 $keepMetadata = &$metadata;
900 break;
901 case 'bc':
902 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal Type mismatch on pass-by-ref args
903 $keepMetadata = array_intersect_key( $metadata, [
904 self::META_INDEXED_TAG_NAME => 1,
905 self::META_SUBELEMENTS => 1,
906 ] );
907 break;
908 default:
909 throw new InvalidArgumentException( __METHOD__ . ': Unknown value for "Strip"' );
910 }
911
912 // No type transformation
913 if ( $transformTypes === null ) {
914 return $data + $keepMetadata;
915 }
916
917 if ( $defaultType === 'array' && $maxKey !== count( $data ) - 1 ) {
918 $defaultType = 'assoc';
919 }
920
921 // Override type, if provided
922 $type = $defaultType;
923 if ( isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] !== 'default' ) {
924 $type = $metadata[self::META_TYPE];
925 }
926 if ( ( $type === 'kvp' || $type === 'BCkvp' ) &&
927 empty( $transformTypes['ArmorKVP'] )
928 ) {
929 $type = 'assoc';
930 } elseif ( $type === 'BCarray' ) {
931 $type = 'array';
932 } elseif ( $type === 'BCassoc' ) {
933 $type = 'assoc';
934 }
935
936 // Apply transformation
937 switch ( $type ) {
938 case 'assoc':
939 $metadata[self::META_TYPE] = 'assoc';
940 $data += $keepMetadata;
941 return empty( $transformTypes['AssocAsObject'] ) ? $data : (object)$data;
942
943 case 'array':
944 // Sort items in ascending order by key. Note that $data may contain a mix of number and string keys,
945 // for which the sorting behavior of krsort() with SORT_REGULAR is inconsistent between PHP versions.
946 // Given a comparison of a string key and a number key, PHP < 8.2 coerces the string key into a number
947 // (which yields zero if the string was non-numeric), and then performs the comparison,
948 // while PHP >= 8.2 makes the behavior consistent with stricter numeric comparisons introduced by
949 // PHP 8.0 in that if the string key is non-numeric, it converts the number key into a string
950 // and compares those two strings instead. We therefore use a custom comparison function
951 // implementing PHP >= 8.2 ordering semantics to ensure consistent ordering of items
952 // irrespective of the PHP version (T326480).
953 uksort( $data, static function ( $a, $b ): int {
954 // In a comparison of a number or numeric string with a non-numeric string,
955 // coerce both values into a string prior to comparing and compare the resulting strings.
956 // Note that PHP prior to 8.0 did not consider numeric strings with trailing whitespace
957 // to be numeric, so trim the inputs prior to the numeric checks to make the behavior
958 // consistent across PHP versions.
959 if ( is_numeric( trim( $a ) ) xor is_numeric( trim( $b ) ) ) {
960 return (string)$a <=> (string)$b;
961 }
962
963 return $a <=> $b;
964 } );
965
966 $data = array_values( $data );
967 $metadata[self::META_TYPE] = 'array';
968 // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
969 return $data + $keepMetadata;
970
971 case 'kvp':
972 case 'BCkvp':
973 $key = $metadata[self::META_KVP_KEY_NAME] ?? $transformTypes['ArmorKVP'];
974 $valKey = isset( $transforms['BC'] ) ? '*' : 'value';
975 $assocAsObject = !empty( $transformTypes['AssocAsObject'] );
976 $merge = !empty( $metadata[self::META_KVP_MERGE] );
977
978 $ret = [];
979 foreach ( $data as $k => $v ) {
980 if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) {
981 $vArr = (array)$v;
982 if ( isset( $vArr[self::META_TYPE] ) ) {
983 $mergeType = $vArr[self::META_TYPE];
984 } elseif ( is_object( $v ) ) {
985 $mergeType = 'assoc';
986 } else {
987 $keys = array_keys( $vArr );
988 sort( $keys, SORT_NUMERIC );
989 $mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc';
990 }
991 } else {
992 $mergeType = 'n/a';
993 }
994 if ( $mergeType === 'assoc' ) {
995 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable vArr set when used
996 $item = $vArr + [
997 $key => $k,
998 ];
999 if ( $strip === 'none' ) {
1000 self::setPreserveKeysList( $item, [ $key ] );
1001 }
1002 } else {
1003 $item = [
1004 $key => $k,
1005 $valKey => $v,
1006 ];
1007 if ( $strip === 'none' ) {
1008 $item += [
1009 self::META_PRESERVE_KEYS => [ $key ],
1010 self::META_CONTENT => $valKey,
1011 self::META_TYPE => 'assoc',
1012 ];
1013 }
1014 }
1015 $ret[] = $assocAsObject ? (object)$item : $item;
1016 }
1017 $metadata[self::META_TYPE] = 'array';
1018
1019 // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
1020 return $ret + $keepMetadata;
1021
1022 default:
1023 throw new UnexpectedValueException( "Unknown type '$type'" );
1024 }
1025 }
1026
1037 public static function stripMetadata( $data ) {
1038 if ( is_array( $data ) || is_object( $data ) ) {
1039 $isObj = is_object( $data );
1040 if ( $isObj ) {
1041 $data = (array)$data;
1042 }
1043 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1044 ? (array)$data[self::META_PRESERVE_KEYS]
1045 : [];
1046 foreach ( $data as $k => $v ) {
1047 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1048 unset( $data[$k] );
1049 } elseif ( is_array( $v ) || is_object( $v ) ) {
1050 $data[$k] = self::stripMetadata( $v );
1051 }
1052 }
1053 if ( $isObj ) {
1054 $data = (object)$data;
1055 }
1056 }
1057 return $data;
1058 }
1059
1071 public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
1072 if ( !is_array( $metadata ) ) {
1073 $metadata = [];
1074 }
1075 if ( is_array( $data ) || is_object( $data ) ) {
1076 $isObj = is_object( $data );
1077 if ( $isObj ) {
1078 $data = (array)$data;
1079 }
1080 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1081 ? (array)$data[self::META_PRESERVE_KEYS]
1082 : [];
1083 foreach ( $data as $k => $v ) {
1084 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1085 $metadata[$k] = $v;
1086 unset( $data[$k] );
1087 }
1088 }
1089 if ( $isObj ) {
1090 $data = (object)$data;
1091 }
1092 }
1093 return $data;
1094 }
1095
1102 private static function size( $value ) {
1103 $s = 0;
1104 if ( is_array( $value ) ) {
1105 foreach ( $value as $k => $v ) {
1106 if ( !self::isMetadataKey( $k ) ) {
1107 $s += self::size( $v );
1108 }
1109 }
1110 } elseif ( is_scalar( $value ) ) {
1111 $s = strlen( $value );
1112 }
1113
1114 return $s;
1115 }
1116
1128 private function &path( $path, $create = 'append' ) {
1129 $path = (array)$path;
1130 $ret = &$this->data;
1131 foreach ( $path as $i => $k ) {
1132 if ( !isset( $ret[$k] ) ) {
1133 switch ( $create ) {
1134 case 'append':
1135 $ret[$k] = [];
1136 break;
1137 case 'prepend':
1138 $ret = [ $k => [] ] + $ret;
1139 break;
1140 case 'dummy':
1141 $tmp = [];
1142 return $tmp;
1143 default:
1144 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1145 throw new InvalidArgumentException( "Path $fail does not exist" );
1146 }
1147 }
1148 if ( !is_array( $ret[$k] ) ) {
1149 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1150 throw new InvalidArgumentException( "Path $fail is not an array" );
1151 }
1152 $ret = &$ret[$k];
1153 }
1154 return $ret;
1155 }
1156
1165 public static function addMetadataToResultVars( $vars, $forceHash = true ) {
1166 // Process subarrays and determine if this is a JS [] or {}
1167 $hash = $forceHash;
1168 $maxKey = -1;
1169 $bools = [];
1170 foreach ( $vars as $k => $v ) {
1171 if ( is_array( $v ) || is_object( $v ) ) {
1172 $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) );
1173 } elseif ( is_bool( $v ) ) {
1174 // Better here to use real bools even in BC formats
1175 $bools[] = $k;
1176 }
1177 if ( is_string( $k ) ) {
1178 $hash = true;
1179 } elseif ( $k > $maxKey ) {
1180 $maxKey = $k;
1181 }
1182 }
1183 if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
1184 $hash = true;
1185 }
1186
1187 // Set metadata appropriately
1188 if ( $hash ) {
1189 // Get the list of keys we actually care about. Unfortunately, we can't support
1190 // certain keys that conflict with ApiResult metadata.
1191 $keys = array_diff( array_keys( $vars ), [
1192 self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME,
1193 self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS
1194 ] );
1195
1196 return [
1197 self::META_TYPE => 'kvp',
1198 self::META_KVP_KEY_NAME => 'key',
1199 self::META_PRESERVE_KEYS => $keys,
1200 self::META_BC_BOOLS => $bools,
1201 self::META_INDEXED_TAG_NAME => 'var',
1202 ] + $vars;
1203 } else {
1204 return [
1205 self::META_TYPE => 'array',
1206 self::META_BC_BOOLS => $bools,
1207 self::META_INDEXED_TAG_NAME => 'value',
1208 ] + $vars;
1209 }
1210 }
1211
1220 public static function formatExpiry( $expiry, $infinity = 'infinity' ) {
1221 static $dbInfinity;
1222 $dbInfinity ??= MediaWikiServices::getInstance()->getConnectionProvider()
1223 ->getReplicaDatabase()
1224 ->getInfinity();
1225
1226 if ( $expiry === '' || $expiry === null || $expiry === false ||
1227 wfIsInfinity( $expiry ) || $expiry === $dbInfinity
1228 ) {
1229 return $infinity;
1230 } else {
1231 return wfTimestamp( TS_ISO_8601, $expiry );
1232 }
1233 }
1234
1235 // endregion -- end of Utility
1236
1237}
1238
1239/*
1240 * This file uses VisualStudio style region/endregion fold markers which are
1241 * recognised by PHPStorm. If modelines are enabled, the following editor
1242 * configuration will also enable folding in vim, if it is in the last 5 lines
1243 * of the file. We also use "@name" which creates sections in Doxygen.
1244 *
1245 * vim: foldmarker=//\ region,//\ endregion foldmethod=marker
1246 */
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:35
addArrayType( $path, $tag, $kvpKeyName=null)
Set the array data type for a path.
static unsetSubelementsList(array &$arr, $names)
Causes the elements with the specified names to be output as attributes (when possible) rather than a...
static unsetPreserveKeysList(array &$arr, $names)
Don't preserve specified keys.
static applyTransformations(array $dataIn, array $transforms)
Apply transformations to an array, returning the transformed array.
const META_TYPE
Key for the 'type' metadata item.
static stripMetadataNonRecursive( $data, &$metadata=null)
Remove metadata keys from a data array or object, non-recursive.
static setArrayType(array &$arr, $type, $kvpKeyName=null)
Set the array data type.
serializeForApiResult()
Allow for adding one ApiResult into another.
static addMetadataToResultVars( $vars, $forceHash=true)
Add the correct metadata to an array of vars we want to export through the API.
static setValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name.
const META_SUBELEMENTS
Key for the 'subelements' metadata item.
Definition ApiResult.php:78
addIndexedTagName( $path, $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.
const META_BC_BOOLS
Key for the 'BC bools' metadata item.
const META_PRESERVE_KEYS
Key for the 'preserve keys' metadata item.
Definition ApiResult.php:84
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:58
addArrayTypeRecursive( $path, $tag, $kvpKeyName=null)
Set the array data type for a path recursively.
getSize()
Get the size of the result, i.e.
__construct( $maxSize)
static unsetValue(array &$arr, $name)
Remove an output value to the array by name.
static setPreserveKeysList(array &$arr, $names)
Preserve specified keys.
addPreserveKeysList( $path, $names)
Preserve specified keys.
addParsedLimit( $moduleName, $limit)
Add the numeric limit for a limit=max to the result.
addSubelementsList( $path, $names)
Causes the elements with the specified names to be output as subelements rather than attributes.
static stripMetadata( $data)
Recursively remove metadata keys from a data array or object.
const META_CONTENT
Key for the 'content' metadata item.
Definition ApiResult.php:90
addIndexedTagNameRecursive( $path, $tag)
Set indexed tag name on $path and all subarrays.
removeSubelementsList( $path, $names)
Causes the elements with the specified names to be output as attributes (when possible) rather than a...
setErrorFormatter(ApiErrorFormatter $formatter)
const OVERRIDE
Override existing value in addValue(), setValue(), and similar functions.
Definition ApiResult.php:41
static setSubelementsList(array &$arr, $names)
Causes the elements with the specified names to be output as subelements rather than attributes.
static setArrayTypeRecursive(array &$arr, $type, $kvpKeyName=null)
Set the array data type recursively.
const META_KVP_KEY_NAME
Key for the metadata item whose value specifies the name used for the kvp key in the alternative outp...
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:49
getResultData( $path=[], $transforms=[])
Get the result data array.
const META_BC_SUBELEMENTS
Key for the 'BC subelements' metadata item.
removePreserveKeysList( $path, $names)
Don't preserve specified keys.
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
const META_INDEXED_TAG_NAME
Key for the 'indexed tag name' metadata item.
Definition ApiResult.php:72
const META_KVP_MERGE
Key for the metadata item that indicates that the KVP key should be added into an assoc value,...
reset()
Clear the current result data.
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 setContentField(array &$arr, $name, $flags=0)
Set the name of the content field name (META_CONTENT)
static formatExpiry( $expiry, $infinity='infinity')
Format an expiry timestamp for API output.
const NO_VALIDATE
For addValue(), setValue() and similar functions, do not validate data.
Definition ApiResult.php:66
addContentValue( $path, $name, $value, $flags=0)
Add value to the output data at the given path and mark as META_CONTENT.
addContentField( $path, $name, $flags=0)
Set the name of the content field name (META_CONTENT)
static isMetadataKey( $key)
Test whether a key should be considered metadata.
Service locator for MediaWiki core services.
This interface allows for overriding the default conversion applied by ApiResult::validateValue().