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