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;
146 private int $size;
148 private $maxSize;
149 private $errorFormatter;
150
154 public function __construct( $maxSize ) {
155 $this->maxSize = $maxSize;
156 $this->reset();
157 }
158
163 public function setErrorFormatter( ApiErrorFormatter $formatter ) {
164 $this->errorFormatter = $formatter;
165 }
166
172 public function serializeForApiResult() {
173 return $this->data;
174 }
175
176 /***************************************************************************/
177 // region Content
183 public function reset() {
184 $this->data = [
185 self::META_TYPE => 'assoc', // Usually what's desired
186 ];
187 $this->size = 0;
188 }
189
243 public function getResultData( $path = [], $transforms = [] ) {
244 $path = (array)$path;
245 if ( !$path ) {
246 return self::applyTransformations( $this->data, $transforms );
247 }
248
249 $last = array_pop( $path );
250 $ret = &$this->path( $path, 'dummy' );
251 if ( !isset( $ret[$last] ) ) {
252 return null;
253 } elseif ( is_array( $ret[$last] ) ) {
254 return self::applyTransformations( $ret[$last], $transforms );
255 } else {
256 return $ret[$last];
257 }
258 }
259
264 public function getSize() {
265 return $this->size;
266 }
267
280 public static function setValue( array &$arr, $name, $value, $flags = 0 ) {
281 if ( ( $flags & self::NO_VALIDATE ) !== self::NO_VALIDATE ) {
282 $value = self::validateValue( $value );
283 }
284
285 if ( $name === null ) {
286 if ( $flags & self::ADD_ON_TOP ) {
287 array_unshift( $arr, $value );
288 } else {
289 $arr[] = $value;
290 }
291 return;
292 }
293
294 $exists = isset( $arr[$name] );
295 if ( !$exists || ( $flags & self::OVERRIDE ) ) {
296 if ( !$exists && ( $flags & self::ADD_ON_TOP ) ) {
297 $arr = [ $name => $value ] + $arr;
298 } else {
299 $arr[$name] = $value;
300 }
301 } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
302 $conflicts = array_intersect_key( $arr[$name], $value );
303 if ( !$conflicts ) {
304 $arr[$name] += $value;
305 } else {
306 $keys = implode( ', ', array_keys( $conflicts ) );
307 throw new RuntimeException(
308 "Conflicting keys ($keys) when attempting to merge element $name"
309 );
310 }
311 } elseif ( $value !== $arr[$name] ) {
312 throw new RuntimeException(
313 "Attempting to add element $name=$value, existing value is {$arr[$name]}"
314 );
315 }
316 }
317
323 private static function validateValue( $value ) {
324 if ( is_object( $value ) ) {
325 // Note we use is_callable() here instead of instanceof because
326 // ApiSerializable is an informal protocol (see docs there for details).
327 if ( is_callable( [ $value, 'serializeForApiResult' ] ) ) {
328 $oldValue = $value;
329 $value = $value->serializeForApiResult();
330 if ( is_object( $value ) ) {
331 throw new UnexpectedValueException(
332 get_class( $oldValue ) . '::serializeForApiResult() returned an object of class ' .
333 get_class( $value )
334 );
335 }
336
337 // Recursive call instead of fall-through so we can throw a
338 // better exception message.
339 try {
340 return self::validateValue( $value );
341 } catch ( Exception $ex ) {
342 throw new UnexpectedValueException(
343 get_class( $oldValue ) . '::serializeForApiResult() returned an invalid value: ' .
344 $ex->getMessage(),
345 0,
346 $ex
347 );
348 }
349 } elseif ( is_callable( [ $value, '__toString' ] ) ) {
350 $value = (string)$value;
351 } else {
352 $value = (array)$value + [ self::META_TYPE => 'assoc' ];
353 }
354 }
355
356 if ( is_string( $value ) ) {
357 // Optimization: avoid querying the service locator for each value.
358 static $contentLanguage = null;
359 if ( !$contentLanguage ) {
360 $contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
361 }
362 $value = $contentLanguage->normalize( $value );
363 } elseif ( is_array( $value ) ) {
364 foreach ( $value as $k => $v ) {
365 $value[$k] = self::validateValue( $v );
366 }
367 } elseif ( $value !== null && !is_scalar( $value ) ) {
368 $type = gettype( $value );
369 // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.is_resource
370 if ( is_resource( $value ) ) {
371 $type .= '(' . get_resource_type( $value ) . ')';
372 }
373 throw new InvalidArgumentException( "Cannot add $type to ApiResult" );
374 } elseif ( is_float( $value ) && !is_finite( $value ) ) {
375 throw new InvalidArgumentException( 'Cannot add non-finite floats to ApiResult' );
376 }
377
378 return $value;
379 }
380
397 public function addValue( $path, $name, $value, $flags = 0 ) {
398 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
399
400 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
401 // self::size needs the validated value. Then flag
402 // to not re-validate later.
403 $value = self::validateValue( $value );
404 $flags |= self::NO_VALIDATE;
405
406 $newsize = $this->size + self::size( $value );
407 if ( $this->maxSize !== false && $newsize > $this->maxSize ) {
408 $this->errorFormatter->addWarning(
409 'result', [ 'apiwarn-truncatedresult', Message::numParam( $this->maxSize ) ]
410 );
411 return false;
412 }
413 $this->size = $newsize;
414 }
415
416 self::setValue( $arr, $name, $value, $flags );
417 return true;
418 }
419
426 public static function unsetValue( array &$arr, $name ) {
427 $ret = null;
428 if ( isset( $arr[$name] ) ) {
429 $ret = $arr[$name];
430 unset( $arr[$name] );
431 }
432 return $ret;
433 }
434
445 public function removeValue( $path, $name, $flags = 0 ) {
446 $path = (array)$path;
447 if ( $name === null ) {
448 if ( !$path ) {
449 throw new InvalidArgumentException( 'Cannot remove the data root' );
450 }
451 $name = array_pop( $path );
452 }
453 $ret = self::unsetValue( $this->path( $path, 'dummy' ), $name );
454 if ( !( $flags & self::NO_SIZE_CHECK ) ) {
455 $newsize = $this->size - self::size( $ret );
456 $this->size = max( $newsize, 0 );
457 }
458 return $ret;
459 }
460
470 public static function setContentValue( array &$arr, $name, $value, $flags = 0 ) {
471 if ( $name === null ) {
472 throw new InvalidArgumentException( 'Content value must be named' );
473 }
474 self::setContentField( $arr, $name, $flags );
475 self::setValue( $arr, $name, $value, $flags );
476 }
477
488 public function addContentValue( $path, $name, $value, $flags = 0 ) {
489 if ( $name === null ) {
490 throw new InvalidArgumentException( 'Content value must be named' );
491 }
492 $this->addContentField( $path, $name, $flags );
493 return $this->addValue( $path, $name, $value, $flags );
494 }
495
503 public function addParsedLimit( $moduleName, $limit ) {
504 // Add value, allowing overwriting
505 $this->addValue( 'limits', $moduleName, $limit,
506 self::OVERRIDE | self::NO_SIZE_CHECK );
507 }
508
509 // endregion -- end of Content
510
511 /***************************************************************************/
512 // region Metadata
523 public static function setContentField( array &$arr, $name, $flags = 0 ) {
524 if ( isset( $arr[self::META_CONTENT] ) &&
525 isset( $arr[$arr[self::META_CONTENT]] ) &&
526 !( $flags & self::OVERRIDE )
527 ) {
528 throw new RuntimeException(
529 "Attempting to set content element as $name when " . $arr[self::META_CONTENT] .
530 ' is already set as the content element'
531 );
532 }
533 $arr[self::META_CONTENT] = $name;
534 }
535
544 public function addContentField( $path, $name, $flags = 0 ) {
545 $arr = &$this->path( $path, ( $flags & self::ADD_ON_TOP ) ? 'prepend' : 'append' );
546 self::setContentField( $arr, $name, $flags );
547 }
548
556 public static function setSubelementsList( array &$arr, $names ) {
557 if ( !isset( $arr[self::META_SUBELEMENTS] ) ) {
558 $arr[self::META_SUBELEMENTS] = (array)$names;
559 } else {
560 $arr[self::META_SUBELEMENTS] = array_merge( $arr[self::META_SUBELEMENTS], (array)$names );
561 }
562 }
563
571 public function addSubelementsList( $path, $names ) {
572 $arr = &$this->path( $path );
573 self::setSubelementsList( $arr, $names );
574 }
575
583 public static function unsetSubelementsList( array &$arr, $names ) {
584 if ( isset( $arr[self::META_SUBELEMENTS] ) ) {
585 $arr[self::META_SUBELEMENTS] = array_diff( $arr[self::META_SUBELEMENTS], (array)$names );
586 }
587 }
588
596 public function removeSubelementsList( $path, $names ) {
597 $arr = &$this->path( $path );
598 self::unsetSubelementsList( $arr, $names );
599 }
600
607 public static function setIndexedTagName( array &$arr, $tag ) {
608 if ( !is_string( $tag ) ) {
609 throw new InvalidArgumentException( 'Bad tag name' );
610 }
611 $arr[self::META_INDEXED_TAG_NAME] = $tag;
612 }
613
620 public function addIndexedTagName( $path, $tag ) {
621 $arr = &$this->path( $path );
622 self::setIndexedTagName( $arr, $tag );
623 }
624
632 public static function setIndexedTagNameRecursive( array &$arr, $tag ) {
633 if ( !is_string( $tag ) ) {
634 throw new InvalidArgumentException( 'Bad tag name' );
635 }
636 $arr[self::META_INDEXED_TAG_NAME] = $tag;
637 foreach ( $arr as $k => &$v ) {
638 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
639 self::setIndexedTagNameRecursive( $v, $tag );
640 }
641 }
642 }
643
651 public function addIndexedTagNameRecursive( $path, $tag ) {
652 $arr = &$this->path( $path );
653 self::setIndexedTagNameRecursive( $arr, $tag );
654 }
655
666 public static function setPreserveKeysList( array &$arr, $names ) {
667 if ( !isset( $arr[self::META_PRESERVE_KEYS] ) ) {
668 $arr[self::META_PRESERVE_KEYS] = (array)$names;
669 } else {
670 $arr[self::META_PRESERVE_KEYS] = array_merge( $arr[self::META_PRESERVE_KEYS], (array)$names );
671 }
672 }
673
681 public function addPreserveKeysList( $path, $names ) {
682 $arr = &$this->path( $path );
683 self::setPreserveKeysList( $arr, $names );
684 }
685
693 public static function unsetPreserveKeysList( array &$arr, $names ) {
694 if ( isset( $arr[self::META_PRESERVE_KEYS] ) ) {
695 $arr[self::META_PRESERVE_KEYS] = array_diff( $arr[self::META_PRESERVE_KEYS], (array)$names );
696 }
697 }
698
706 public function removePreserveKeysList( $path, $names ) {
707 $arr = &$this->path( $path );
708 self::unsetPreserveKeysList( $arr, $names );
709 }
710
719 public static function setArrayType( array &$arr, $type, $kvpKeyName = null ) {
720 if ( !in_array( $type, [
721 'default', 'array', 'assoc', 'kvp', 'BCarray', 'BCassoc', 'BCkvp'
722 ], true ) ) {
723 throw new InvalidArgumentException( 'Bad type' );
724 }
725 $arr[self::META_TYPE] = $type;
726 if ( is_string( $kvpKeyName ) ) {
727 $arr[self::META_KVP_KEY_NAME] = $kvpKeyName;
728 }
729 }
730
738 public function addArrayType( $path, $tag, $kvpKeyName = null ) {
739 $arr = &$this->path( $path );
740 self::setArrayType( $arr, $tag, $kvpKeyName );
741 }
742
750 public static function setArrayTypeRecursive( array &$arr, $type, $kvpKeyName = null ) {
751 self::setArrayType( $arr, $type, $kvpKeyName );
752 foreach ( $arr as $k => &$v ) {
753 if ( is_array( $v ) && !self::isMetadataKey( $k ) ) {
754 self::setArrayTypeRecursive( $v, $type, $kvpKeyName );
755 }
756 }
757 }
758
766 public function addArrayTypeRecursive( $path, $tag, $kvpKeyName = null ) {
767 $arr = &$this->path( $path );
768 self::setArrayTypeRecursive( $arr, $tag, $kvpKeyName );
769 }
770
771 // endregion -- end of Metadata
772
773 /***************************************************************************/
774 // region Utility
783 public static function isMetadataKey( $key ) {
784 // Optimization: This is a very hot and highly optimized code path. Note that ord() only
785 // considers the first character and also works with empty strings and integers.
786 // 95 corresponds to the '_' character.
787 return ord( $key ) === 95;
788 }
789
799 protected static function applyTransformations( array $dataIn, array $transforms ) {
800 $strip = $transforms['Strip'] ?? 'none';
801 if ( $strip === 'base' ) {
802 $transforms['Strip'] = 'none';
803 }
804 $transformTypes = $transforms['Types'] ?? null;
805 if ( $transformTypes !== null && !is_array( $transformTypes ) ) {
806 throw new InvalidArgumentException( __METHOD__ . ':Value for "Types" must be an array' );
807 }
808
809 $metadata = [];
810 $data = self::stripMetadataNonRecursive( $dataIn, $metadata );
811
812 if ( isset( $transforms['Custom'] ) ) {
813 if ( !is_callable( $transforms['Custom'] ) ) {
814 throw new InvalidArgumentException( __METHOD__ . ': Value for "Custom" must be callable' );
815 }
816 call_user_func_array( $transforms['Custom'], [ &$data, &$metadata ] );
817 }
818
819 if ( ( isset( $transforms['BC'] ) || $transformTypes !== null ) &&
820 isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] === 'BCkvp' &&
821 !isset( $metadata[self::META_KVP_KEY_NAME] )
822 ) {
823 throw new UnexpectedValueException( 'Type "BCkvp" used without setting ' .
824 'ApiResult::META_KVP_KEY_NAME metadata item' );
825 }
826
827 // BC transformations
828 $boolKeys = null;
829 if ( isset( $transforms['BC'] ) ) {
830 if ( !is_array( $transforms['BC'] ) ) {
831 throw new InvalidArgumentException( __METHOD__ . ':Value for "BC" must be an array' );
832 }
833 if ( !in_array( 'nobool', $transforms['BC'], true ) ) {
834 $boolKeys = isset( $metadata[self::META_BC_BOOLS] )
835 ? array_fill_keys( $metadata[self::META_BC_BOOLS], true )
836 : [];
837 }
838
839 if ( !in_array( 'no*', $transforms['BC'], true ) &&
840 isset( $metadata[self::META_CONTENT] ) && $metadata[self::META_CONTENT] !== '*'
841 ) {
842 $k = $metadata[self::META_CONTENT];
843 $data['*'] = $data[$k];
844 unset( $data[$k] );
845 $metadata[self::META_CONTENT] = '*';
846 }
847
848 if ( !in_array( 'nosub', $transforms['BC'], true ) &&
849 isset( $metadata[self::META_BC_SUBELEMENTS] )
850 ) {
851 foreach ( $metadata[self::META_BC_SUBELEMENTS] as $k ) {
852 if ( isset( $data[$k] ) ) {
853 $data[$k] = [
854 '*' => $data[$k],
855 self::META_CONTENT => '*',
856 self::META_TYPE => 'assoc',
857 ];
858 }
859 }
860 }
861
862 if ( isset( $metadata[self::META_TYPE] ) ) {
863 switch ( $metadata[self::META_TYPE] ) {
864 case 'BCarray':
865 case 'BCassoc':
866 $metadata[self::META_TYPE] = 'default';
867 break;
868 case 'BCkvp':
869 $transformTypes['ArmorKVP'] = $metadata[self::META_KVP_KEY_NAME];
870 break;
871 }
872 }
873 }
874
875 // Figure out type, do recursive calls, and do boolean transform if necessary
876 $defaultType = 'array';
877 $maxKey = -1;
878 foreach ( $data as $k => &$v ) {
879 $v = is_array( $v ) ? self::applyTransformations( $v, $transforms ) : $v;
880 if ( $boolKeys !== null && is_bool( $v ) && !isset( $boolKeys[$k] ) ) {
881 if ( !$v ) {
882 unset( $data[$k] );
883 continue;
884 }
885 $v = '';
886 }
887 if ( is_string( $k ) ) {
888 $defaultType = 'assoc';
889 } elseif ( $k > $maxKey ) {
890 $maxKey = $k;
891 }
892 }
893 unset( $v );
894
895 // Determine which metadata to keep
896 switch ( $strip ) {
897 case 'all':
898 case 'base':
899 $keepMetadata = [];
900 break;
901 case 'none':
902 $keepMetadata = &$metadata;
903 break;
904 case 'bc':
905 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal Type mismatch on pass-by-ref args
906 $keepMetadata = array_intersect_key( $metadata, [
907 self::META_INDEXED_TAG_NAME => 1,
908 self::META_SUBELEMENTS => 1,
909 ] );
910 break;
911 default:
912 throw new InvalidArgumentException( __METHOD__ . ': Unknown value for "Strip"' );
913 }
914
915 // No type transformation
916 if ( $transformTypes === null ) {
917 return $data + $keepMetadata;
918 }
919
920 if ( $defaultType === 'array' && $maxKey !== count( $data ) - 1 ) {
921 $defaultType = 'assoc';
922 }
923
924 // Override type, if provided
925 $type = $defaultType;
926 if ( isset( $metadata[self::META_TYPE] ) && $metadata[self::META_TYPE] !== 'default' ) {
927 $type = $metadata[self::META_TYPE];
928 }
929 if ( ( $type === 'kvp' || $type === 'BCkvp' ) &&
930 empty( $transformTypes['ArmorKVP'] )
931 ) {
932 $type = 'assoc';
933 } elseif ( $type === 'BCarray' ) {
934 $type = 'array';
935 } elseif ( $type === 'BCassoc' ) {
936 $type = 'assoc';
937 }
938
939 // Apply transformation
940 switch ( $type ) {
941 case 'assoc':
942 $metadata[self::META_TYPE] = 'assoc';
943 $data += $keepMetadata;
944 return empty( $transformTypes['AssocAsObject'] ) ? $data : (object)$data;
945
946 case 'array':
947 // Sort items in ascending order by key. Note that $data may contain a mix of number and string keys,
948 // for which the sorting behavior of krsort() with SORT_REGULAR is inconsistent between PHP versions.
949 // Given a comparison of a string key and a number key, PHP < 8.2 coerces the string key into a number
950 // (which yields zero if the string was non-numeric), and then performs the comparison,
951 // while PHP >= 8.2 makes the behavior consistent with stricter numeric comparisons introduced by
952 // PHP 8.0 in that if the string key is non-numeric, it converts the number key into a string
953 // and compares those two strings instead. We therefore use a custom comparison function
954 // implementing PHP >= 8.2 ordering semantics to ensure consistent ordering of items
955 // irrespective of the PHP version (T326480).
956 uksort( $data, static function ( $a, $b ): int {
957 // In a comparison of a number or numeric string with a non-numeric string,
958 // coerce both values into a string prior to comparing and compare the resulting strings.
959 // Note that PHP prior to 8.0 did not consider numeric strings with trailing whitespace
960 // to be numeric, so trim the inputs prior to the numeric checks to make the behavior
961 // consistent across PHP versions.
962 if ( is_numeric( trim( $a ) ) xor is_numeric( trim( $b ) ) ) {
963 return (string)$a <=> (string)$b;
964 }
965
966 return $a <=> $b;
967 } );
968
969 $data = array_values( $data );
970 $metadata[self::META_TYPE] = 'array';
971 // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
972 return $data + $keepMetadata;
973
974 case 'kvp':
975 case 'BCkvp':
976 $key = $metadata[self::META_KVP_KEY_NAME] ?? $transformTypes['ArmorKVP'];
977 $valKey = isset( $transforms['BC'] ) ? '*' : 'value';
978 $assocAsObject = !empty( $transformTypes['AssocAsObject'] );
979 $merge = !empty( $metadata[self::META_KVP_MERGE] );
980
981 $ret = [];
982 foreach ( $data as $k => $v ) {
983 if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) {
984 $vArr = (array)$v;
985 if ( isset( $vArr[self::META_TYPE] ) ) {
986 $mergeType = $vArr[self::META_TYPE];
987 } elseif ( is_object( $v ) ) {
988 $mergeType = 'assoc';
989 } else {
990 $keys = array_keys( $vArr );
991 sort( $keys, SORT_NUMERIC );
992 $mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc';
993 }
994 } else {
995 $mergeType = 'n/a';
996 }
997 if ( $mergeType === 'assoc' ) {
998 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable vArr set when used
999 $item = $vArr + [
1000 $key => $k,
1001 ];
1002 if ( $strip === 'none' ) {
1003 self::setPreserveKeysList( $item, [ $key ] );
1004 }
1005 } else {
1006 $item = [
1007 $key => $k,
1008 $valKey => $v,
1009 ];
1010 if ( $strip === 'none' ) {
1011 $item += [
1012 self::META_PRESERVE_KEYS => [ $key ],
1013 self::META_CONTENT => $valKey,
1014 self::META_TYPE => 'assoc',
1015 ];
1016 }
1017 }
1018 $ret[] = $assocAsObject ? (object)$item : $item;
1019 }
1020 $metadata[self::META_TYPE] = 'array';
1021
1022 // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
1023 return $ret + $keepMetadata;
1024
1025 default:
1026 throw new UnexpectedValueException( "Unknown type '$type'" );
1027 }
1028 }
1029
1040 public static function stripMetadata( $data ) {
1041 if ( is_array( $data ) || is_object( $data ) ) {
1042 $isObj = is_object( $data );
1043 if ( $isObj ) {
1044 $data = (array)$data;
1045 }
1046 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1047 ? (array)$data[self::META_PRESERVE_KEYS]
1048 : [];
1049 foreach ( $data as $k => $v ) {
1050 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1051 unset( $data[$k] );
1052 } elseif ( is_array( $v ) || is_object( $v ) ) {
1053 $data[$k] = self::stripMetadata( $v );
1054 }
1055 }
1056 if ( $isObj ) {
1057 $data = (object)$data;
1058 }
1059 }
1060 return $data;
1061 }
1062
1074 public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
1075 if ( !is_array( $metadata ) ) {
1076 $metadata = [];
1077 }
1078 if ( is_array( $data ) || is_object( $data ) ) {
1079 $isObj = is_object( $data );
1080 if ( $isObj ) {
1081 $data = (array)$data;
1082 }
1083 $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1084 ? (array)$data[self::META_PRESERVE_KEYS]
1085 : [];
1086 foreach ( $data as $k => $v ) {
1087 if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1088 $metadata[$k] = $v;
1089 unset( $data[$k] );
1090 }
1091 }
1092 if ( $isObj ) {
1093 $data = (object)$data;
1094 }
1095 }
1096 return $data;
1097 }
1098
1105 private static function size( $value ) {
1106 $s = 0;
1107 if ( is_array( $value ) ) {
1108 foreach ( $value as $k => $v ) {
1109 if ( !self::isMetadataKey( $k ) ) {
1110 $s += self::size( $v );
1111 }
1112 }
1113 } elseif ( is_scalar( $value ) ) {
1114 $s = strlen( $value );
1115 }
1116
1117 return $s;
1118 }
1119
1131 private function &path( $path, $create = 'append' ) {
1132 $path = (array)$path;
1133 $ret = &$this->data;
1134 foreach ( $path as $i => $k ) {
1135 if ( !isset( $ret[$k] ) ) {
1136 switch ( $create ) {
1137 case 'append':
1138 $ret[$k] = [];
1139 break;
1140 case 'prepend':
1141 $ret = [ $k => [] ] + $ret;
1142 break;
1143 case 'dummy':
1144 $tmp = [];
1145 return $tmp;
1146 default:
1147 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1148 throw new InvalidArgumentException( "Path $fail does not exist" );
1149 }
1150 }
1151 if ( !is_array( $ret[$k] ) ) {
1152 $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1153 throw new InvalidArgumentException( "Path $fail is not an array" );
1154 }
1155 $ret = &$ret[$k];
1156 }
1157 return $ret;
1158 }
1159
1168 public static function addMetadataToResultVars( $vars, $forceHash = true ) {
1169 // Process subarrays and determine if this is a JS [] or {}
1170 $hash = $forceHash;
1171 $maxKey = -1;
1172 $bools = [];
1173 foreach ( $vars as $k => $v ) {
1174 if ( is_array( $v ) || is_object( $v ) ) {
1175 $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) );
1176 } elseif ( is_bool( $v ) ) {
1177 // Better here to use real bools even in BC formats
1178 $bools[] = $k;
1179 }
1180 if ( is_string( $k ) ) {
1181 $hash = true;
1182 } elseif ( $k > $maxKey ) {
1183 $maxKey = $k;
1184 }
1185 }
1186 if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
1187 $hash = true;
1188 }
1189
1190 // Set metadata appropriately
1191 if ( $hash ) {
1192 // Get the list of keys we actually care about. Unfortunately, we can't support
1193 // certain keys that conflict with ApiResult metadata.
1194 $keys = array_diff( array_keys( $vars ), [
1195 self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME,
1196 self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS
1197 ] );
1198
1199 return [
1200 self::META_TYPE => 'kvp',
1201 self::META_KVP_KEY_NAME => 'key',
1202 self::META_PRESERVE_KEYS => $keys,
1203 self::META_BC_BOOLS => $bools,
1204 self::META_INDEXED_TAG_NAME => 'var',
1205 ] + $vars;
1206 } else {
1207 return [
1208 self::META_TYPE => 'array',
1209 self::META_BC_BOOLS => $bools,
1210 self::META_INDEXED_TAG_NAME => 'value',
1211 ] + $vars;
1212 }
1213 }
1214
1223 public static function formatExpiry( $expiry, $infinity = 'infinity' ) {
1224 static $dbInfinity;
1225 $dbInfinity ??= MediaWikiServices::getInstance()->getConnectionProvider()
1226 ->getReplicaDatabase()
1227 ->getInfinity();
1228
1229 if ( $expiry === '' || $expiry === null || $expiry === false ||
1230 wfIsInfinity( $expiry ) || $expiry === $dbInfinity
1231 ) {
1232 return $infinity;
1233 } else {
1234 return wfTimestamp( TS_ISO_8601, $expiry );
1235 }
1236 }
1237
1238 // endregion -- end of Utility
1239
1240}
1241
1242/*
1243 * This file uses VisualStudio style region/endregion fold markers which are
1244 * recognised by PHPStorm. If modelines are enabled, the following editor
1245 * configuration will also enable folding in vim, if it is in the last 5 lines
1246 * of the file. We also use "@name" which creates sections in Doxygen.
1247 *
1248 * vim: foldmarker=//\ region,//\ endregion foldmethod=marker
1249 */
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().