MediaWiki  master
ApiResult.php
Go to the documentation of this file.
1 <?php
22 
35 class 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 ) ) {
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  ksort( $data );
945  $data = array_values( $data );
946  $metadata[self::META_TYPE] = 'array';
947  // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
948  return $data + $keepMetadata;
949 
950  case 'kvp':
951  case 'BCkvp':
952  $key = $metadata[self::META_KVP_KEY_NAME] ?? $transformTypes['ArmorKVP'];
953  $valKey = isset( $transforms['BC'] ) ? '*' : 'value';
954  $assocAsObject = !empty( $transformTypes['AssocAsObject'] );
955  $merge = !empty( $metadata[self::META_KVP_MERGE] );
956 
957  $ret = [];
958  foreach ( $data as $k => $v ) {
959  if ( $merge && ( is_array( $v ) || is_object( $v ) ) ) {
960  $vArr = (array)$v;
961  if ( isset( $vArr[self::META_TYPE] ) ) {
962  $mergeType = $vArr[self::META_TYPE];
963  } elseif ( is_object( $v ) ) {
964  $mergeType = 'assoc';
965  } else {
966  $keys = array_keys( $vArr );
967  sort( $keys, SORT_NUMERIC );
968  $mergeType = ( $keys === array_keys( $keys ) ) ? 'array' : 'assoc';
969  }
970  } else {
971  $mergeType = 'n/a';
972  }
973  if ( $mergeType === 'assoc' ) {
974  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable vArr set when used
975  $item = $vArr + [
976  $key => $k,
977  ];
978  if ( $strip === 'none' ) {
979  self::setPreserveKeysList( $item, [ $key ] );
980  }
981  } else {
982  $item = [
983  $key => $k,
984  $valKey => $v,
985  ];
986  if ( $strip === 'none' ) {
987  $item += [
988  self::META_PRESERVE_KEYS => [ $key ],
989  self::META_CONTENT => $valKey,
990  self::META_TYPE => 'assoc',
991  ];
992  }
993  }
994  $ret[] = $assocAsObject ? (object)$item : $item;
995  }
996  $metadata[self::META_TYPE] = 'array';
997 
998  // @phan-suppress-next-line PhanTypeMismatchReturnNullable Type mismatch on pass-by-ref args
999  return $ret + $keepMetadata;
1000 
1001  default:
1002  throw new UnexpectedValueException( "Unknown type '$type'" );
1003  }
1004  }
1005 
1016  public static function stripMetadata( $data ) {
1017  if ( is_array( $data ) || is_object( $data ) ) {
1018  $isObj = is_object( $data );
1019  if ( $isObj ) {
1020  $data = (array)$data;
1021  }
1022  $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1023  ? (array)$data[self::META_PRESERVE_KEYS]
1024  : [];
1025  foreach ( $data as $k => $v ) {
1026  if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1027  unset( $data[$k] );
1028  } elseif ( is_array( $v ) || is_object( $v ) ) {
1029  $data[$k] = self::stripMetadata( $v );
1030  }
1031  }
1032  if ( $isObj ) {
1033  $data = (object)$data;
1034  }
1035  }
1036  return $data;
1037  }
1038 
1050  public static function stripMetadataNonRecursive( $data, &$metadata = null ) {
1051  if ( !is_array( $metadata ) ) {
1052  $metadata = [];
1053  }
1054  if ( is_array( $data ) || is_object( $data ) ) {
1055  $isObj = is_object( $data );
1056  if ( $isObj ) {
1057  $data = (array)$data;
1058  }
1059  $preserveKeys = isset( $data[self::META_PRESERVE_KEYS] )
1060  ? (array)$data[self::META_PRESERVE_KEYS]
1061  : [];
1062  foreach ( $data as $k => $v ) {
1063  if ( self::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
1064  $metadata[$k] = $v;
1065  unset( $data[$k] );
1066  }
1067  }
1068  if ( $isObj ) {
1069  $data = (object)$data;
1070  }
1071  }
1072  return $data;
1073  }
1074 
1081  private static function size( $value ) {
1082  $s = 0;
1083  if ( is_array( $value ) ) {
1084  foreach ( $value as $k => $v ) {
1085  if ( !self::isMetadataKey( $k ) ) {
1086  $s += self::size( $v );
1087  }
1088  }
1089  } elseif ( is_scalar( $value ) ) {
1090  $s = strlen( $value );
1091  }
1092 
1093  return $s;
1094  }
1095 
1107  private function &path( $path, $create = 'append' ) {
1108  $path = (array)$path;
1109  $ret = &$this->data;
1110  foreach ( $path as $i => $k ) {
1111  if ( !isset( $ret[$k] ) ) {
1112  switch ( $create ) {
1113  case 'append':
1114  $ret[$k] = [];
1115  break;
1116  case 'prepend':
1117  $ret = [ $k => [] ] + $ret;
1118  break;
1119  case 'dummy':
1120  $tmp = [];
1121  return $tmp;
1122  default:
1123  $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1124  throw new InvalidArgumentException( "Path $fail does not exist" );
1125  }
1126  }
1127  if ( !is_array( $ret[$k] ) ) {
1128  $fail = implode( '.', array_slice( $path, 0, $i + 1 ) );
1129  throw new InvalidArgumentException( "Path $fail is not an array" );
1130  }
1131  $ret = &$ret[$k];
1132  }
1133  return $ret;
1134  }
1135 
1144  public static function addMetadataToResultVars( $vars, $forceHash = true ) {
1145  // Process subarrays and determine if this is a JS [] or {}
1146  $hash = $forceHash;
1147  $maxKey = -1;
1148  $bools = [];
1149  foreach ( $vars as $k => $v ) {
1150  if ( is_array( $v ) || is_object( $v ) ) {
1151  $vars[$k] = self::addMetadataToResultVars( (array)$v, is_object( $v ) );
1152  } elseif ( is_bool( $v ) ) {
1153  // Better here to use real bools even in BC formats
1154  $bools[] = $k;
1155  }
1156  if ( is_string( $k ) ) {
1157  $hash = true;
1158  } elseif ( $k > $maxKey ) {
1159  $maxKey = $k;
1160  }
1161  }
1162  if ( !$hash && $maxKey !== count( $vars ) - 1 ) {
1163  $hash = true;
1164  }
1165 
1166  // Set metadata appropriately
1167  if ( $hash ) {
1168  // Get the list of keys we actually care about. Unfortunately, we can't support
1169  // certain keys that conflict with ApiResult metadata.
1170  $keys = array_diff( array_keys( $vars ), [
1171  self::META_TYPE, self::META_PRESERVE_KEYS, self::META_KVP_KEY_NAME,
1172  self::META_INDEXED_TAG_NAME, self::META_BC_BOOLS
1173  ] );
1174 
1175  return [
1176  self::META_TYPE => 'kvp',
1177  self::META_KVP_KEY_NAME => 'key',
1178  self::META_PRESERVE_KEYS => $keys,
1179  self::META_BC_BOOLS => $bools,
1180  self::META_INDEXED_TAG_NAME => 'var',
1181  ] + $vars;
1182  } else {
1183  return [
1184  self::META_TYPE => 'array',
1185  self::META_BC_BOOLS => $bools,
1186  self::META_INDEXED_TAG_NAME => 'value',
1187  ] + $vars;
1188  }
1189  }
1190 
1199  public static function formatExpiry( $expiry, $infinity = 'infinity' ) {
1200  static $dbInfinity;
1201  $dbInfinity ??= MediaWikiServices::getInstance()->getDBLoadBalancerFactory()
1202  ->getReplicaDatabase()
1203  ->getInfinity();
1204 
1205  if ( $expiry === '' || $expiry === null || $expiry === false ||
1206  wfIsInfinity( $expiry ) || $expiry === $dbInfinity
1207  ) {
1208  return $infinity;
1209  } else {
1210  return wfTimestamp( TS_ISO_8601, $expiry );
1211  }
1212  }
1213 
1214  // endregion -- end of Utility
1215 
1216 }
1217 
1218 /*
1219  * This file uses VisualStudio style region/endregion fold markers which are
1220  * recognised by PHPStorm. If modelines are enabled, the following editor
1221  * configuration will also enable folding in vim, if it is in the last 5 lines
1222  * of the file. We also use "@name" which creates sections in Doxygen.
1223  *
1224  * vim: foldmarker=//\ region,//\ endregion foldmethod=marker
1225  */
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.
Definition: ApiResult.php:735
static unsetSubelementsList(array &$arr, $names)
Causes the elements with the specified names to be output as attributes (when possible) rather than a...
Definition: ApiResult.php:580
static unsetPreserveKeysList(array &$arr, $names)
Don't preserve specified keys.
Definition: ApiResult.php:690
static applyTransformations(array $dataIn, array $transforms)
Apply transformations to an array, returning the transformed array.
Definition: ApiResult.php:796
const META_TYPE
Key for the 'type' metadata item.
Definition: ApiResult.php:110
static stripMetadataNonRecursive( $data, &$metadata=null)
Remove metadata keys from a data array or object, non-recursive.
Definition: ApiResult.php:1050
static setArrayType(array &$arr, $type, $kvpKeyName=null)
Set the array data type.
Definition: ApiResult.php:716
serializeForApiResult()
Allow for adding one ApiResult into another.
Definition: ApiResult.php:169
static addMetadataToResultVars( $vars, $forceHash=true)
Add the correct metadata to an array of vars we want to export through the API.
Definition: ApiResult.php:1144
static setValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name.
Definition: ApiResult.php:277
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.
Definition: ApiResult.php:617
addValue( $path, $name, $value, $flags=0)
Add value to the output data at the given path.
Definition: ApiResult.php:394
const META_BC_BOOLS
Key for the 'BC bools' metadata item.
Definition: ApiResult.php:136
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.
Definition: ApiResult.php:763
getSize()
Get the size of the result, i.e.
Definition: ApiResult.php:261
__construct( $maxSize)
Definition: ApiResult.php:151
static unsetValue(array &$arr, $name)
Remove an output value to the array by name.
Definition: ApiResult.php:423
static setPreserveKeysList(array &$arr, $names)
Preserve specified keys.
Definition: ApiResult.php:663
addPreserveKeysList( $path, $names)
Preserve specified keys.
Definition: ApiResult.php:678
addParsedLimit( $moduleName, $limit)
Add the numeric limit for a limit=max to the result.
Definition: ApiResult.php:500
addSubelementsList( $path, $names)
Causes the elements with the specified names to be output as subelements rather than attributes.
Definition: ApiResult.php:568
static stripMetadata( $data)
Recursively remove metadata keys from a data array or object.
Definition: ApiResult.php:1016
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.
Definition: ApiResult.php:648
removeSubelementsList( $path, $names)
Causes the elements with the specified names to be output as attributes (when possible) rather than a...
Definition: ApiResult.php:593
setErrorFormatter(ApiErrorFormatter $formatter)
Definition: ApiResult.php:160
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.
Definition: ApiResult.php:553
static setArrayTypeRecursive(array &$arr, $type, $kvpKeyName=null)
Set the array data type recursively.
Definition: ApiResult.php:747
const META_KVP_KEY_NAME
Key for the metadata item whose value specifies the name used for the kvp key in the alternative outp...
Definition: ApiResult.php:119
removeValue( $path, $name, $flags=0)
Remove value from the output data at the given path.
Definition: ApiResult.php:442
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.
Definition: ApiResult.php:240
const META_BC_SUBELEMENTS
Key for the 'BC subelements' metadata item.
Definition: ApiResult.php:143
removePreserveKeysList( $path, $names)
Don't preserve specified keys.
Definition: ApiResult.php:703
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:604
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,...
Definition: ApiResult.php:129
reset()
Clear the current result data.
Definition: ApiResult.php:180
static setContentValue(array &$arr, $name, $value, $flags=0)
Add an output value to the array by name and mark as META_CONTENT.
Definition: ApiResult.php:467
static setIndexedTagNameRecursive(array &$arr, $tag)
Set indexed tag name on $arr and all subarrays.
Definition: ApiResult.php:629
static setContentField(array &$arr, $name, $flags=0)
Set the name of the content field name (META_CONTENT)
Definition: ApiResult.php:520
static formatExpiry( $expiry, $infinity='infinity')
Format an expiry timestamp for API output.
Definition: ApiResult.php:1199
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.
Definition: ApiResult.php:485
addContentField( $path, $name, $flags=0)
Set the name of the content field name (META_CONTENT)
Definition: ApiResult.php:541
static isMetadataKey( $key)
Test whether a key should be considered metadata.
Definition: ApiResult.php:780
Service locator for MediaWiki core services.
static numParam( $num)
Definition: Message.php:1147
This interface allows for overriding the default conversion applied by ApiResult::validateValue().