Move some validation logic from ApiStructureTest to ParamValidator

ApiStructureTest has a lot of logic for validating Action API settings
arrays during CI. Some of that logic should be part of ParamValidator
instead.

Bug: T242887
Change-Id: I3c3d23e38456de19179ae3e5855397316b6e4c40
Depends-On: I04de72d731b94468d8a12b35df67f359382b3742
This commit is contained in:
Brad Jorsch 2020-01-15 16:08:43 -05:00 committed by Jforrester
parent c2b1525908
commit d4c2f0d899
32 changed files with 2579 additions and 523 deletions

View file

@ -59,6 +59,39 @@ class NamespaceDef extends EnumDef {
return parent::normalizeSettings( $settings );
}
public function checkSettings( string $name, $settings, array $options, array $ret ) : array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
self::PARAM_EXTRA_NAMESPACES,
] );
if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
( $settings[ParamValidator::PARAM_ALL] ?? true ) !== true &&
!isset( $ret['issues'][ParamValidator::PARAM_ALL] )
) {
$ret['issues'][ParamValidator::PARAM_ALL] =
'PARAM_ALL cannot be false or a string for namespace-type parameters';
}
$ns = $settings[self::PARAM_EXTRA_NAMESPACES] ?? [];
if ( !is_array( $ns ) ) {
$type = gettype( $ns );
} elseif ( $ns === [] ) {
$type = 'integer[]';
} else {
$types = array_unique( array_map( 'gettype', $ns ) );
$type = implode( '|', $types );
$type = count( $types ) > 1 ? "($type)[]" : "{$type}[]";
}
if ( $type !== 'integer[]' ) {
$ret['issues'][self::PARAM_EXTRA_NAMESPACES] =
"PARAM_EXTRA_NAMESPACES must be an integer[], got $type";
}
return $ret;
}
public function getParamInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );

View file

@ -75,6 +75,55 @@ class UserDef extends TypeDef {
return parent::normalizeSettings( $settings );
}
public function checkSettings( string $name, $settings, array $options, array $ret ) : array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
self::PARAM_ALLOWED_USER_TYPES, self::PARAM_RETURN_OBJECT,
] );
if ( !is_bool( $settings[self::PARAM_RETURN_OBJECT] ?? false ) ) {
$ret['issues'][self::PARAM_RETURN_OBJECT] = 'PARAM_RETURN_OBJECT must be boolean, got '
. gettype( $settings[self::PARAM_RETURN_OBJECT] );
}
$hasId = false;
if ( isset( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
if ( !is_array( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
$ret['issues'][self::PARAM_ALLOWED_USER_TYPES] = 'PARAM_ALLOWED_USER_TYPES must be an array, '
. 'got ' . gettype( $settings[self::PARAM_ALLOWED_USER_TYPES] );
} elseif ( $settings[self::PARAM_ALLOWED_USER_TYPES] === [] ) {
$ret['issues'][self::PARAM_ALLOWED_USER_TYPES] = 'PARAM_ALLOWED_USER_TYPES cannot be empty';
} else {
$bad = array_diff(
$settings[self::PARAM_ALLOWED_USER_TYPES],
[ 'name', 'ip', 'cidr', 'interwiki', 'id' ]
);
if ( $bad ) {
$ret['issues'][self::PARAM_ALLOWED_USER_TYPES] =
'PARAM_ALLOWED_USER_TYPES contains invalid values: ' . implode( ', ', $bad );
}
$hasId = in_array( 'id', $settings[self::PARAM_ALLOWED_USER_TYPES], true );
}
}
if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
( $hasId || !empty( $settings[self::PARAM_RETURN_OBJECT] ) ) &&
(
( $settings[ParamValidator::PARAM_ISMULTI_LIMIT1] ?? 100 ) > 10 ||
( $settings[ParamValidator::PARAM_ISMULTI_LIMIT2] ?? 100 ) > 10
)
) {
$ret['issues'][] = 'Multi-valued user-type parameters with PARAM_RETURN_OBJECT or allowing IDs '
. 'should set low values (<= 10) for PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2.'
. ' (Note that "<= 10" is arbitrary. If something hits this, we can investigate a real limit '
. 'once we have a real use case to look at.)';
}
return $ret;
}
/**
* Process $value to a UserIdentity, if possible
* @param string $value

View file

@ -98,21 +98,11 @@ class ApiParamValidator {
}
/**
* Adjust certain settings where ParamValidator differs from historical Action API behavior
* @param array|mixed $settings
* Map deprecated styles for messages for ParamValidator
* @param array $settings
* @return array
*/
public function normalizeSettings( $settings ) : array {
$settings = $this->paramValidator->normalizeSettings( $settings );
if ( !isset( $settings[ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES] ) ) {
$settings[ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES] = true;
}
if ( !isset( $settings[IntegerDef::PARAM_IGNORE_RANGE] ) ) {
$settings[IntegerDef::PARAM_IGNORE_RANGE] = empty( $settings[ApiBase::PARAM_RANGE_ENFORCE] );
}
private function mapDeprecatedSettingsMessages( array $settings ) : array {
if ( isset( $settings[EnumDef::PARAM_DEPRECATED_VALUES] ) ) {
foreach ( $settings[EnumDef::PARAM_DEPRECATED_VALUES] as &$v ) {
if ( $v === null || $v === true || $v instanceof MessageValue ) {
@ -137,6 +127,193 @@ class ApiParamValidator {
return $settings;
}
/**
* Adjust certain settings where ParamValidator differs from historical Action API behavior
* @param array|mixed $settings
* @return array
*/
public function normalizeSettings( $settings ) : array {
if ( is_array( $settings ) ) {
if ( !isset( $settings[ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES] ) ) {
$settings[ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES] = true;
}
if ( !isset( $settings[IntegerDef::PARAM_IGNORE_RANGE] ) ) {
$settings[IntegerDef::PARAM_IGNORE_RANGE] = empty( $settings[ApiBase::PARAM_RANGE_ENFORCE] );
}
$settings = $this->mapDeprecatedSettingsMessages( $settings );
}
return $this->paramValidator->normalizeSettings( $settings );
}
/**
* Check an API settings message
* @param ApiBase $module
* @param string $key
* @param mixed $value
* @param array &$ret
*/
private function checkSettingsMessage( ApiBase $module, string $key, $value, array &$ret ) : void {
$msg = ApiBase::makeMessage( $value, $module );
if ( $msg instanceof Message ) {
$ret['messages'][] = $this->messageConverter->convertMessage( $msg );
} else {
$ret['issues'][] = "Message specification for $key is not valid";
}
}
/**
* Check settings for the Action API.
* @param ApiBase $module
* @param array $params All module params to test
* @param string $name Parameter to test
* @param array $options Options array
* @return array As for ParamValidator::checkSettings()
*/
public function checkSettings(
ApiBase $module, array $params, string $name, array $options
) : array {
$options['module'] = $module;
$settings = $params[$name];
if ( is_array( $settings ) ) {
$settings = $this->mapDeprecatedSettingsMessages( $settings );
}
$ret = $this->paramValidator->checkSettings(
$module->encodeParamName( $name ), $settings, $options
);
$ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
ApiBase::PARAM_RANGE_ENFORCE, ApiBase::PARAM_HELP_MSG, ApiBase::PARAM_HELP_MSG_APPEND,
ApiBase::PARAM_HELP_MSG_INFO, ApiBase::PARAM_HELP_MSG_PER_VALUE, ApiBase::PARAM_TEMPLATE_VARS,
] );
if ( !is_array( $settings ) ) {
$settings = [];
}
if ( array_key_exists( ApiBase::PARAM_VALUE_LINKS, $settings ) ) {
$ret['issues'][ApiBase::PARAM_VALUE_LINKS]
= 'PARAM_VALUE_LINKS was deprecated in MediaWiki 1.35';
}
if ( !is_bool( $settings[ApiBase::PARAM_RANGE_ENFORCE] ?? false ) ) {
$ret['issues'][ApiBase::PARAM_RANGE_ENFORCE] = 'PARAM_RANGE_ENFORCE must be boolean, got '
. gettype( $settings[ApiBase::PARAM_RANGE_ENFORCE] );
}
if ( isset( $settings[ApiBase::PARAM_HELP_MSG] ) ) {
$this->checkSettingsMessage(
$module, 'PARAM_HELP_MSG', $settings[ApiBase::PARAM_HELP_MSG], $ret
);
}
if ( isset( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ) ) {
if ( !is_array( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ) ) {
$ret['issues'][ApiBase::PARAM_HELP_MSG_APPEND] = 'PARAM_HELP_MSG_APPEND must be an array, got '
. gettype( $settings[ApiBase::PARAM_HELP_MSG_APPEND] );
} else {
foreach ( $settings[ApiBase::PARAM_HELP_MSG_APPEND] as $k => $v ) {
$this->checkSettingsMessage( $module, "PARAM_HELP_MSG_APPEND[$k]", $v, $ret );
}
}
}
if ( isset( $settings[ApiBase::PARAM_HELP_MSG_INFO] ) ) {
if ( !is_array( $settings[ApiBase::PARAM_HELP_MSG_INFO] ) ) {
$ret['issues'][ApiBase::PARAM_HELP_MSG_INFO] = 'PARAM_HELP_MSG_INFO must be an array, got '
. gettype( $settings[ApiBase::PARAM_HELP_MSG_INFO] );
} else {
$path = $module->getModulePath();
foreach ( $settings[ApiBase::PARAM_HELP_MSG_INFO] as $k => $v ) {
if ( !is_array( $v ) ) {
$ret['issues'][] = "PARAM_HELP_MSG_INFO[$k] must be an array, got " . gettype( $v );
} elseif ( !is_string( $v[0] ) ) {
$ret['issues'][] = "PARAM_HELP_MSG_INFO[$k][0] must be a string, got " . gettype( $v[0] );
} else {
$v[0] = "apihelp-{$path}-paraminfo-{$v[0]}";
$this->checkSettingsMessage( $module, "PARAM_HELP_MSG_INFO[$k]", $v, $ret );
}
}
}
}
if ( isset( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) {
if ( !is_array( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) {
$ret['issues'][ApiBase::PARAM_HELP_MSG_PER_VALUE] = 'PARAM_HELP_MSG_PER_VALUE must be an array,'
. ' got ' . gettype( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] );
} elseif ( !is_array( $settings[ParamValidator::PARAM_TYPE] ?? '' ) ) {
$ret['issues'][ApiBase::PARAM_HELP_MSG_PER_VALUE] = 'PARAM_HELP_MSG_PER_VALUE can only be used '
. 'with PARAM_TYPE as an array';
} else {
$values = array_map( 'strval', $settings[ParamValidator::PARAM_TYPE] );
foreach ( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] as $k => $v ) {
if ( !in_array( (string)$k, $values, true ) ) {
// Or should this be allowed?
$ret['issues'][] = "PARAM_HELP_MSG_PER_VALUE contains \"$k\", which is not in PARAM_TYPE.";
}
$this->checkSettingsMessage( $module, "PARAM_HELP_MSG_PER_VALUE[$k]", $v, $ret );
}
}
}
if ( isset( $settings[ApiBase::PARAM_TEMPLATE_VARS] ) ) {
if ( !is_array( $settings[ApiBase::PARAM_TEMPLATE_VARS] ) ) {
$ret['issues'][ApiBase::PARAM_TEMPLATE_VARS] = 'PARAM_TEMPLATE_VARS must be an array,'
. ' got ' . gettype( $settings[ApiBase::PARAM_TEMPLATE_VARS] );
} elseif ( $settings[ApiBase::PARAM_TEMPLATE_VARS] === [] ) {
$ret['issues'][ApiBase::PARAM_TEMPLATE_VARS] = 'PARAM_TEMPLATE_VARS cannot be the empty array';
} else {
foreach ( $settings[ApiBase::PARAM_TEMPLATE_VARS] as $key => $target ) {
if ( !preg_match( '/^[^{}]+$/', $key ) ) {
$ret['issues'][] = "PARAM_TEMPLATE_VARS keys may not contain '{' or '}', got \"$key\"";
} elseif ( strpos( $name, '{' . $key . '}' ) === false ) {
$ret['issues'][] = "Parameter name must contain PARAM_TEMPLATE_VARS key {{$key}}";
}
if ( !is_string( $target ) && !is_int( $target ) ) {
$ret['issues'][] = "PARAM_TEMPLATE_VARS[$key] has invalid target type " . gettype( $target );
} elseif ( !isset( $params[$target] ) ) {
$ret['issues'][] = "PARAM_TEMPLATE_VARS[$key] target parameter \"$target\" does not exist";
} else {
$settings2 = $params[$target];
if ( empty( $settings2[ParamValidator::PARAM_ISMULTI] ) ) {
$ret['issues'][] = "PARAM_TEMPLATE_VARS[$key] target parameter \"$target\" must have "
. 'PARAM_ISMULTI = true';
}
if ( isset( $settings2[ApiBase::PARAM_TEMPLATE_VARS] ) ) {
if ( $target === $name ) {
$ret['issues'][] = "PARAM_TEMPLATE_VARS[$key] cannot target the parameter itself";
}
if ( array_diff(
$settings2[ApiBase::PARAM_TEMPLATE_VARS],
$settings[ApiBase::PARAM_TEMPLATE_VARS]
) ) {
$ret['issues'][] = "PARAM_TEMPLATE_VARS[$key]: Target's "
. 'PARAM_TEMPLATE_VARS must be a subset of the original';
}
}
}
}
$keys = implode( '|', array_map(
function ( $key ) {
return preg_quote( $key, '/' );
},
array_keys( $settings[ApiBase::PARAM_TEMPLATE_VARS] )
) );
if ( !preg_match( '/^(?>[^{}]+|\{(?:' . $keys . ')\})+$/', $name ) ) {
$ret['issues'][] = "Parameter name may not contain '{' or '}' other than '
. 'as defined by PARAM_TEMPLATE_VARS";
}
}
} elseif ( !preg_match( '/^[^{}]+$/', $name ) ) {
$ret['issues'][] = "Parameter name may not contain '{' or '}' without PARAM_TEMPLATE_VARS";
}
return $ret;
}
/**
* Convert a ValidationException to an ApiUsageException
* @param ApiBase $module

View file

@ -30,6 +30,49 @@ class SubmoduleDef extends EnumDef {
*/
public const PARAM_SUBMODULE_PARAM_PREFIX = 'param-submodule-param-prefix';
public function checkSettings( string $name, $settings, array $options, array $ret ) : array {
$map = $settings[self::PARAM_SUBMODULE_MAP] ?? [];
if ( !is_array( $map ) ) {
$ret['issues'][self::PARAM_SUBMODULE_MAP] = 'PARAM_SUBMODULE_MAP must be an array, got '
. gettype( $map );
// Prevent errors in parent::checkSettings()
$settings[self::PARAM_SUBMODULE_MAP] = null;
}
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
self::PARAM_SUBMODULE_MAP, self::PARAM_SUBMODULE_PARAM_PREFIX,
] );
if ( is_array( $map ) ) {
$module = $options['module'];
foreach ( $map as $k => $v ) {
if ( !is_string( $v ) ) {
$ret['issues'][] = 'Values for PARAM_SUBMODULE_MAP must be strings, '
. "but value for \"$k\" is " . gettype( $v );
continue;
}
try {
$submod = $module->getModuleFromPath( $v );
} catch ( ApiUsageException $ex ) {
$submod = null;
}
if ( !$submod ) {
$ret['issues'][] = "PARAM_SUBMODULE_MAP contains \"$v\", which is not a valid module path";
}
}
}
if ( !is_string( $settings[self::PARAM_SUBMODULE_PARAM_PREFIX] ?? '' ) ) {
$ret['issues'][self::PARAM_SUBMODULE_PARAM_PREFIX] = 'PARAM_SUBMODULE_PARAM_PREFIX must be '
. 'a string, got ' . gettype( $settings[self::PARAM_SUBMODULE_PARAM_PREFIX] );
}
return $ret;
}
public function getEnumValues( $name, array $settings, array $options ) {
if ( isset( $settings[self::PARAM_SUBMODULE_MAP] ) ) {
$modules = array_keys( $settings[self::PARAM_SUBMODULE_MAP] );

View file

@ -332,12 +332,11 @@ class ParamValidator {
}
/**
* Normalize a parameter settings array
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* Logic shared by normalizeSettings() and checkSettings()
* @param array|mixed $settings
* @return array
*/
public function normalizeSettings( $settings ) {
private function normalizeSettingsInternal( $settings ) {
// Shorthand
if ( !is_array( $settings ) ) {
$settings = [
@ -350,6 +349,18 @@ class ParamValidator {
$settings[self::PARAM_TYPE] = gettype( $settings[self::PARAM_DEFAULT] ?? null );
}
return $settings;
}
/**
* Normalize a parameter settings array
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @return array
*/
public function normalizeSettings( $settings ) {
$settings = $this->normalizeSettingsInternal( $settings );
$typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
if ( $typeDef ) {
$settings = $typeDef->normalizeSettings( $settings );
@ -358,6 +369,136 @@ class ParamValidator {
return $settings;
}
/**
* Validate a parameter settings array
*
* This is intended for validation of parameter settings during unit or
* integration testing, and should implement strict checks.
*
* The rest of the code should generally be more permissive.
*
* @param string $name Parameter name
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @param array $options Options array, passed through to the TypeDef and Callbacks.
* @return array
* - 'issues': (string[]) Errors detected in $settings, as English text. If the settings
* are valid, this will be the empty array.
* - 'allowedKeys': (string[]) ParamValidator keys that are allowed in `$settings`.
* - 'messages': (MessageValue[]) Messages to be checked for existence.
*/
public function checkSettings( string $name, $settings, array $options ) : array {
$settings = $this->normalizeSettingsInternal( $settings );
$issues = [];
$allowedKeys = [
self::PARAM_TYPE, self::PARAM_DEFAULT, self::PARAM_REQUIRED, self::PARAM_ISMULTI,
self::PARAM_SENSITIVE, self::PARAM_DEPRECATED, self::PARAM_IGNORE_UNRECOGNIZED_VALUES,
];
$messages = [];
$type = $settings[self::PARAM_TYPE];
$typeDef = null;
if ( !is_string( $type ) && !is_array( $type ) ) {
$issues[self::PARAM_TYPE] = 'PARAM_TYPE must be a string or array, got ' . gettype( $type );
} else {
$typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
if ( !$typeDef ) {
if ( is_array( $type ) ) {
$type = 'enum';
}
$issues[self::PARAM_TYPE] = "Unknown/unregistered PARAM_TYPE \"$type\"";
}
}
if ( isset( $settings[self::PARAM_DEFAULT] ) ) {
try {
$this->validateValue(
$name, $settings[self::PARAM_DEFAULT], $settings, [ 'is-default' => true ] + $options
);
} catch ( ValidationException $ex ) {
$issues[self::PARAM_DEFAULT] = 'Value for PARAM_DEFAULT does not validate (code '
. $ex->getFailureMessage()->getCode() . ')';
}
}
if ( !is_bool( $settings[self::PARAM_REQUIRED] ?? false ) ) {
$issues[self::PARAM_REQUIRED] = 'PARAM_REQUIRED must be boolean, got '
. gettype( $settings[self::PARAM_REQUIRED] );
}
if ( !is_bool( $settings[self::PARAM_ISMULTI] ?? false ) ) {
$issues[self::PARAM_ISMULTI] = 'PARAM_ISMULTI must be boolean, got '
. gettype( $settings[self::PARAM_ISMULTI] );
}
if ( !empty( $settings[self::PARAM_ISMULTI] ) ) {
$allowedKeys = array_merge( $allowedKeys, [
self::PARAM_ISMULTI_LIMIT1, self::PARAM_ISMULTI_LIMIT2,
self::PARAM_ALL, self::PARAM_ALLOW_DUPLICATES
] );
$limit1 = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1;
$limit2 = $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2;
if ( !is_int( $limit1 ) ) {
$issues[self::PARAM_ISMULTI_LIMIT1] = 'PARAM_ISMULTI_LIMIT1 must be an integer, got '
. gettype( $settings[self::PARAM_ISMULTI_LIMIT1] );
} elseif ( $limit1 <= 0 ) {
$issues[self::PARAM_ISMULTI_LIMIT1] =
"PARAM_ISMULTI_LIMIT1 must be greater than 0, got $limit1";
}
if ( !is_int( $limit2 ) ) {
$issues[self::PARAM_ISMULTI_LIMIT2] = 'PARAM_ISMULTI_LIMIT2 must be an integer, got '
. gettype( $settings[self::PARAM_ISMULTI_LIMIT2] );
} elseif ( $limit2 < $limit1 ) {
$issues[self::PARAM_ISMULTI_LIMIT2] =
'PARAM_ISMULTI_LIMIT2 must be greater than or equal to PARAM_ISMULTI_LIMIT1, but '
. "$limit2 < $limit1";
}
$all = $settings[self::PARAM_ALL] ?? false;
if ( !is_string( $all ) && !is_bool( $all ) ) {
$issues[self::PARAM_ALL] = 'PARAM_ALL must be a string or boolean, got ' . gettype( $all );
} elseif ( $all !== false && $typeDef ) {
if ( $all === true ) {
$all = self::ALL_DEFAULT_STRING;
}
$values = $typeDef->getEnumValues( $name, $settings, $options );
if ( !is_array( $values ) ) {
$issues[self::PARAM_ALL] = 'PARAM_ALL cannot be used with non-enumerated types';
} elseif ( in_array( $all, $values, true ) ) {
$issues[self::PARAM_ALL] = 'Value for PARAM_ALL conflicts with an enumerated value';
}
}
if ( !is_bool( $settings[self::PARAM_ALLOW_DUPLICATES] ?? false ) ) {
$issues[self::PARAM_ALLOW_DUPLICATES] = 'PARAM_ALLOW_DUPLICATES must be boolean, got '
. gettype( $settings[self::PARAM_ALLOW_DUPLICATES] );
}
}
if ( !is_bool( $settings[self::PARAM_SENSITIVE] ?? false ) ) {
$issues[self::PARAM_SENSITIVE] = 'PARAM_SENSITIVE must be boolean, got '
. gettype( $settings[self::PARAM_SENSITIVE] );
}
if ( !is_bool( $settings[self::PARAM_DEPRECATED] ?? false ) ) {
$issues[self::PARAM_DEPRECATED] = 'PARAM_DEPRECATED must be boolean, got '
. gettype( $settings[self::PARAM_DEPRECATED] );
}
if ( !is_bool( $settings[self::PARAM_IGNORE_UNRECOGNIZED_VALUES] ?? false ) ) {
$issues[self::PARAM_IGNORE_UNRECOGNIZED_VALUES] = 'PARAM_IGNORE_UNRECOGNIZED_VALUES must be '
. 'boolean, got ' . gettype( $settings[self::PARAM_IGNORE_UNRECOGNIZED_VALUES] );
}
$ret = [ 'issues' => $issues, 'allowedKeys' => $allowedKeys, 'messages' => $messages ];
if ( $typeDef ) {
$ret = $typeDef->checkSettings( $name, $settings, $options, $ret );
}
return $ret;
}
/**
* Fetch and valiate a parameter value using a settings array
*

View file

@ -132,6 +132,32 @@ abstract class TypeDef {
return $settings;
}
/**
* Validate a parameter settings array
*
* This is intended for validation of parameter settings during unit or
* integration testing, and should implement strict checks.
*
* The rest of the code should generally be more permissive.
*
* @see ParamValidator::checkSettings()
* @param string $name Parameter name
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @param array $options Options array, passed through to the TypeDef and Callbacks.
* @param array $ret
* - 'issues': (string[]) Errors detected in $settings, as English text. If the settings
* are valid, this will be the empty array. Keys on input are ParamValidator constants,
* allowing the typedef to easily override core validation; this need not be preserved
* when returned.
* - 'allowedKeys': (string[]) ParamValidator keys that are allowed in `$settings`.
* - 'messages': (MessageValue[]) Messages to be checked for existence.
* @return array $ret, with any relevant changes.
*/
public function checkSettings( string $name, $settings, array $options, array $ret ) : array {
return $ret;
}
/**
* Get the values for enum-like parameters
*

View file

@ -84,6 +84,37 @@ class EnumDef extends TypeDef {
);
}
public function checkSettings( string $name, $settings, array $options, array $ret ) : array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'][] = self::PARAM_DEPRECATED_VALUES;
$dv = $settings[self::PARAM_DEPRECATED_VALUES] ?? [];
if ( !is_array( $dv ?? false ) ) {
$ret['issues'][self::PARAM_DEPRECATED_VALUES] = 'PARAM_DEPRECATED_VALUES must be an array, got '
. gettype( $dv );
} else {
$values = array_map( function ( $v ) use ( $name, $settings, $options ) {
return $this->stringifyValue( $name, $v, $settings, $options );
}, $this->getEnumValues( $name, $settings, $options ) );
foreach ( $dv as $k => $v ) {
$k = $this->stringifyValue( $name, $k, $settings, $options );
if ( !in_array( $k, $values, true ) ) {
$ret['issues'][] = "PARAM_DEPRECATED_VALUES contains \"$k\", which is not "
. 'one of the enumerated values';
} elseif ( $v instanceof MessageValue ) {
$ret['messages'][] = $v;
} elseif ( $v !== null && $v !== true ) {
$type = $v === false ? 'false' : ( is_object( $v ) ? get_class( $v ) : gettype( $v ) );
$ret['issues'][] = 'Values in PARAM_DEPRECATED_VALUES must be null, true, or MessageValue, '
. "but value for \"$k\" is $type";
}
}
}
return $ret;
}
public function getEnumValues( $name, array $settings, array $options ) {
return array_values( $settings[ParamValidator::PARAM_TYPE] );
}

View file

@ -28,6 +28,8 @@ use Wikimedia\ParamValidator\ParamValidator;
*/
class FloatDef extends NumericDef {
protected $valueType = 'double';
public function validate( $name, $value, array $settings, array $options ) {
// Use a regex so as to avoid any potential oddness PHP's default conversion might allow.
if ( !preg_match( '/^[+-]?(?:\d*\.)?\d+(?:[eE][+-]?\d+)?$/D', $value ) ) {

View file

@ -48,6 +48,26 @@ class LimitDef extends IntegerDef {
return parent::normalizeSettings( $settings );
}
public function checkSettings( string $name, $settings, array $options, array $ret ) : array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
!isset( $ret['issues'][ParamValidator::PARAM_ISMULTI] )
) {
$ret['issues'][ParamValidator::PARAM_ISMULTI] =
'PARAM_ISMULTI cannot be used for limit-type parameters';
}
if ( ( $settings[self::PARAM_MIN] ?? 0 ) < 0 ) {
$ret['issues'][] = 'PARAM_MIN must be greater than or equal to 0';
}
if ( !isset( $settings[self::PARAM_MAX] ) ) {
$ret['issues'][] = 'PARAM_MAX must be set';
}
return $ret;
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getHelpInfo( $name, $settings, $options );

View file

@ -46,6 +46,9 @@ abstract class NumericDef extends TypeDef {
*/
const PARAM_MAX2 = 'param-max2';
/** @var string PHP type (as from `gettype()`) of values this NumericDef handles */
protected $valueType = 'integer';
/**
* Check the range of a value
* @param int|float $value Value to check.
@ -116,6 +119,46 @@ abstract class NumericDef extends TypeDef {
return parent::normalizeSettings( $settings );
}
public function checkSettings( string $name, $settings, array $options, array $ret ) : array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
self::PARAM_IGNORE_RANGE, self::PARAM_MIN, self::PARAM_MAX, self::PARAM_MAX2,
] );
if ( !is_bool( $settings[self::PARAM_IGNORE_RANGE] ?? false ) ) {
$ret['issues'][self::PARAM_IGNORE_RANGE] = 'PARAM_IGNORE_RANGE must be boolean, got '
. gettype( $settings[self::PARAM_IGNORE_RANGE] );
}
$min = $settings[self::PARAM_MIN] ?? null;
$max = $settings[self::PARAM_MAX] ?? null;
$max2 = $settings[self::PARAM_MAX2] ?? null;
if ( $min !== null && gettype( $min ) !== $this->valueType ) {
$ret['issues'][self::PARAM_MIN] = "PARAM_MIN must be $this->valueType, got " . gettype( $min );
}
if ( $max !== null && gettype( $max ) !== $this->valueType ) {
$ret['issues'][self::PARAM_MAX] = "PARAM_MAX must be $this->valueType, got " . gettype( $max );
}
if ( $max2 !== null && gettype( $max2 ) !== $this->valueType ) {
$ret['issues'][self::PARAM_MAX2] = "PARAM_MAX2 must be $this->valueType, got "
. gettype( $max2 );
}
if ( $min !== null && $max !== null && $min > $max ) {
$ret['issues'][] = "PARAM_MIN must be less than or equal to PARAM_MAX, but $min > $max";
}
if ( $max2 !== null ) {
if ( $max === null ) {
$ret['issues'][] = 'PARAM_MAX2 cannot be used without PARAM_MAX';
} elseif ( $max2 < $max ) {
$ret['issues'][] = "PARAM_MAX2 must be greater than or equal to PARAM_MAX, but $max2 < $max";
}
}
return $ret;
}
public function getParamInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );

View file

@ -20,4 +20,17 @@ class PasswordDef extends StringDef {
return parent::normalizeSettings( $settings );
}
public function checkSettings( string $name, $settings, array $options, array $ret ) : array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
if ( ( $settings[ParamValidator::PARAM_SENSITIVE] ?? true ) !== true &&
!isset( $ret['issues'][ParamValidator::PARAM_SENSITIVE] )
) {
$ret['issues'][ParamValidator::PARAM_SENSITIVE] =
'Cannot set PARAM_SENSITIVE to false for password-type parameters';
}
return $ret;
}
}

View file

@ -35,6 +35,26 @@ class PresenceBooleanDef extends TypeDef {
return parent::normalizeSettings( $settings );
}
public function checkSettings( string $name, $settings, array $options, array $ret ) : array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
!isset( $ret['issues'][ParamValidator::PARAM_ISMULTI] )
) {
$ret['issues'][ParamValidator::PARAM_ISMULTI] =
'PARAM_ISMULTI cannot be used for presence-boolean-type parameters';
}
if ( ( $settings[ParamValidator::PARAM_DEFAULT] ?? false ) !== false &&
!isset( $ret['issues'][ParamValidator::PARAM_DEFAULT] )
) {
$ret['issues'][ParamValidator::PARAM_DEFAULT] =
'Default for presence-boolean-type parameters must be false or null';
}
return $ret;
}
public function getParamInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );

View file

@ -90,6 +90,43 @@ class StringDef extends TypeDef {
return $value;
}
public function checkSettings( string $name, $settings, array $options, array $ret ) : array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
self::PARAM_MAX_BYTES, self::PARAM_MAX_CHARS,
] );
$maxb = $settings[self::PARAM_MAX_BYTES] ?? PHP_INT_MAX;
if ( !is_int( $maxb ) ) {
$ret['issues'][self::PARAM_MAX_BYTES] = 'PARAM_MAX_BYTES must be an integer, got '
. gettype( $maxb );
} elseif ( $maxb < 0 ) {
$ret['issues'][self::PARAM_MAX_BYTES] = 'PARAM_MAX_BYTES must be greater than or equal to 0';
}
$maxc = $settings[self::PARAM_MAX_CHARS] ?? PHP_INT_MAX;
if ( !is_int( $maxc ) ) {
$ret['issues'][self::PARAM_MAX_CHARS] = 'PARAM_MAX_CHARS must be an integer, got '
. gettype( $maxc );
} elseif ( $maxc < 0 ) {
$ret['issues'][self::PARAM_MAX_CHARS] = 'PARAM_MAX_CHARS must be greater than or equal to 0';
}
if ( !$this->allowEmptyWhenRequired && !empty( $settings[ParamValidator::PARAM_REQUIRED] ) ) {
if ( $maxb === 0 ) {
$ret['issues'][] = 'PARAM_REQUIRED is set, allowEmptyWhenRequired is not set, and '
. 'PARAM_MAX_BYTES is 0. That\'s impossible to satisfy.';
}
if ( $maxc === 0 ) {
$ret['issues'][] = 'PARAM_REQUIRED is set, allowEmptyWhenRequired is not set, and '
. 'PARAM_MAX_CHARS is 0. That\'s impossible to satisfy.';
}
}
return $ret;
}
public function getParamInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );

View file

@ -2,6 +2,7 @@
namespace Wikimedia\ParamValidator\TypeDef;
use InvalidArgumentException;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\Callbacks;
use Wikimedia\ParamValidator\ParamValidator;
@ -61,6 +62,16 @@ class TimestampDef extends TypeDef {
$this->defaultFormat = $options['defaultFormat'] ?? 'ConvertibleTimestamp';
$this->stringifyFormat = $options['stringifyFormat'] ?? TS_ISO_8601;
// Check values by trying to convert 0
if ( $this->defaultFormat !== 'ConvertibleTimestamp' && $this->defaultFormat !== 'DateTime' &&
ConvertibleTimestamp::convert( $this->defaultFormat, 0 ) === false
) {
throw new InvalidArgumentException( 'Invalid value for $options[\'defaultFormat\']' );
}
if ( ConvertibleTimestamp::convert( $this->stringifyFormat, 0 ) === false ) {
throw new InvalidArgumentException( 'Invalid value for $options[\'stringifyFormat\']' );
}
}
public function validate( $name, $value, array $settings, array $options ) {
@ -94,6 +105,23 @@ class TimestampDef extends TypeDef {
}
}
public function checkSettings( string $name, $settings, array $options, array $ret ) : array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
$ret['allowedKeys'] = array_merge( $ret['allowedKeys'], [
self::PARAM_TIMESTAMP_FORMAT,
] );
$f = $settings[self::PARAM_TIMESTAMP_FORMAT] ?? $this->defaultFormat;
if ( $f !== 'ConvertibleTimestamp' && $f !== 'DateTime' &&
ConvertibleTimestamp::convert( $f, 0 ) === false
) {
$ret['issues'][self::PARAM_TIMESTAMP_FORMAT] = 'Value for PARAM_TIMESTAMP_FORMAT is not valid';
}
return $ret;
}
public function stringifyValue( $name, $value, array $settings, array $options ) {
if ( !$value instanceof ConvertibleTimestamp ) {
$value = new ConvertibleTimestamp( $value );

View file

@ -122,6 +122,24 @@ class UploadDef extends TypeDef {
}
}
public function checkSettings( string $name, $settings, array $options, array $ret ) : array {
$ret = parent::checkSettings( $name, $settings, $options, $ret );
if ( isset( $settings[ParamValidator::PARAM_DEFAULT] ) ) {
$ret['issues'][ParamValidator::PARAM_DEFAULT] =
'Cannot specify a default for upload-type parameters';
}
if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) &&
!isset( $ret['issues'][ParamValidator::PARAM_ISMULTI] )
) {
$ret['issues'][ParamValidator::PARAM_ISMULTI] =
'PARAM_ISMULTI cannot be used for upload-type parameters';
}
return $ret;
}
public function stringifyValue( $name, $value, array $settings, array $options ) {
// Not going to happen.
return null;

View file

@ -7,6 +7,7 @@ use MediaWiki\MediaWikiServices;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\TypeDef\EnumDef;
use Wikimedia\ParamValidator\TypeDef\TypeDefTestCase;
use Wikimedia\ParamValidator\ValidationException;
@ -95,6 +96,149 @@ class NamespaceDefTest extends TypeDefTestCase {
];
}
public function provideCheckSettings() {
$keys = [ 'Y', EnumDef::PARAM_DEPRECATED_VALUES, NamespaceDef::PARAM_EXTRA_NAMESPACES ];
return [
'Basic test' => [
[],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with everything' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ALL => true,
NamespaceDef::PARAM_EXTRA_NAMESPACES => [ -1, -2 ],
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_ALL cannot be false' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ALL => false,
],
self::STDRET,
[
'issues' => [
'X',
ParamValidator::PARAM_ALL
=> 'PARAM_ALL cannot be false or a string for namespace-type parameters',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_ALL cannot be a string' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ALL => 'all',
],
self::STDRET,
[
'issues' => [
'X',
ParamValidator::PARAM_ALL
=> 'PARAM_ALL cannot be false or a string for namespace-type parameters',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_ALL ignored without PARAM_ISMULTI' => [
[
ParamValidator::PARAM_ALL => 'all',
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_ALL cannot be a string, but another PARAM_ALL issue was already logged' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ALL => 'all',
],
[
'issues' => [ ParamValidator::PARAM_ALL => 'XXX' ],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [ ParamValidator::PARAM_ALL => 'XXX' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Bad type for PARAM_EXTRA_NAMESPACES' => [
[
NamespaceDef::PARAM_EXTRA_NAMESPACES => -1,
],
self::STDRET,
[
'issues' => [
'X',
NamespaceDef::PARAM_EXTRA_NAMESPACES
=> 'PARAM_EXTRA_NAMESPACES must be an integer[], got integer'
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Empty array for PARAM_EXTRA_NAMESPACES ok' => [
[
NamespaceDef::PARAM_EXTRA_NAMESPACES => [],
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Bad value types for PARAM_EXTRA_NAMESPACES' => [
[
NamespaceDef::PARAM_EXTRA_NAMESPACES => [ '-1' ],
],
self::STDRET,
[
'issues' => [
'X',
NamespaceDef::PARAM_EXTRA_NAMESPACES
=> 'PARAM_EXTRA_NAMESPACES must be an integer[], got string[]'
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Bad value types for PARAM_EXTRA_NAMESPACES (2)' => [
[
NamespaceDef::PARAM_EXTRA_NAMESPACES => [ 0, '-1', '-2' ],
],
self::STDRET,
[
'issues' => [
'X',
NamespaceDef::PARAM_EXTRA_NAMESPACES
=> 'PARAM_EXTRA_NAMESPACES must be an integer[], got (integer|string)[]'
],
'allowedKeys' => $keys,
'messages' => [],
],
],
];
}
public function provideStringifyValue() {
return [
'Basic test' => [ 123, '123' ],

View file

@ -161,6 +161,160 @@ class UserDefTest extends TypeDefTestCase {
];
}
public function provideCheckSettings() {
$keys = [ 'Y', UserDef::PARAM_ALLOWED_USER_TYPES, UserDef::PARAM_RETURN_OBJECT ];
$ismultiIssue = 'Multi-valued user-type parameters with PARAM_RETURN_OBJECT or allowing IDs '
. 'should set low values (<= 10) for PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2.'
. ' (Note that "<= 10" is arbitrary. If something hits this, we can investigate a real limit '
. 'once we have a real use case to look at.)';
return [
'Basic test' => [
[],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with everything' => [
[
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Bad types' => [
[
UserDef::PARAM_ALLOWED_USER_TYPES => 'name',
UserDef::PARAM_RETURN_OBJECT => 1,
],
self::STDRET,
[
'issues' => [
'X',
UserDef::PARAM_RETURN_OBJECT => 'PARAM_RETURN_OBJECT must be boolean, got integer',
UserDef::PARAM_ALLOWED_USER_TYPES => 'PARAM_ALLOWED_USER_TYPES must be an array, got string',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_ALLOWED_USER_TYPES cannot be empty' => [
[
UserDef::PARAM_ALLOWED_USER_TYPES => [],
],
self::STDRET,
[
'issues' => [
'X',
UserDef::PARAM_ALLOWED_USER_TYPES => 'PARAM_ALLOWED_USER_TYPES cannot be empty',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_ALLOWED_USER_TYPES invalid values' => [
[
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id', 'ssn', 'Q-number' ],
],
self::STDRET,
[
'issues' => [
'X',
UserDef::PARAM_ALLOWED_USER_TYPES
=> 'PARAM_ALLOWED_USER_TYPES contains invalid values: ssn, Q-number',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'ISMULTI generally ok' => [
[
ParamValidator::PARAM_ISMULTI => true,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'ISMULTI with ID not ok (1)' => [
[
ParamValidator::PARAM_ISMULTI => true,
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ],
],
self::STDRET,
[
'issues' => [ 'X', $ismultiIssue ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'ISMULTI with ID not ok (2)' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ISMULTI_LIMIT1 => 10,
ParamValidator::PARAM_ISMULTI_LIMIT2 => 11,
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ],
],
self::STDRET,
[
'issues' => [ 'X', $ismultiIssue ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'ISMULTI with ID ok with low limits' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ISMULTI_LIMIT1 => 10,
ParamValidator::PARAM_ISMULTI_LIMIT2 => 10,
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ],
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'ISMULTI with RETURN_OBJECT also not ok' => [
[
ParamValidator::PARAM_ISMULTI => true,
UserDef::PARAM_RETURN_OBJECT => true,
],
self::STDRET,
[
'issues' => [ 'X', $ismultiIssue ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'ISMULTI with RETURN_OBJECT also ok with low limits' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ISMULTI_LIMIT1 => 10,
ParamValidator::PARAM_ISMULTI_LIMIT2 => 10,
UserDef::PARAM_RETURN_OBJECT => true,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
];
}
public function provideGetInfo() {
return [
'Basic test' => [

View file

@ -135,6 +135,398 @@ class ApiParamValidatorTest extends ApiTestCase {
];
}
/**
* @dataProvider provideCheckSettings
* @param array $params All module parameters.
* @param string $name Parameter to test.
* @param array $expect
*/
public function testCheckSettings( array $params, string $name, array $expect ) : void {
[ $validator, $main ] = $this->getValidator( new FauxRequest( [] ) );
$module = $main->getModuleFromPath( 'query+allpages' );
$mock = $this->getMockBuilder( ParamValidator::class )
->disableOriginalConstructor()
->setMethods( [ 'checkSettings' ] )
->getMock();
$mock->expects( $this->once() )->method( 'checkSettings' )
->willReturnCallback( function ( $n, $settings, $options ) use ( $name, $module ) {
$this->assertSame( "ap$name", $n );
$this->assertSame( [ 'module' => $module ], $options );
$ret = [ 'issues' => [ 'X' ], 'allowedKeys' => [ 'Y' ], 'messages' => [] ];
$stack = is_array( $settings ) ? [ &$settings ] : [];
while ( $stack ) {
foreach ( $stack[0] as $k => $v ) {
if ( $v instanceof MessageValue ) {
$ret['messages'][] = $v;
} elseif ( is_array( $v ) ) {
$stack[] = &$stack[0][$k];
}
}
array_shift( $stack );
}
return $ret;
} );
TestingAccessWrapper::newFromObject( $validator )->paramValidator = $mock;
$this->assertEquals( $expect, $validator->checkSettings( $module, $params, $name, [] ) );
}
public function provideCheckSettings() {
$keys = [
'Y', ApiBase::PARAM_RANGE_ENFORCE, ApiBase::PARAM_HELP_MSG, ApiBase::PARAM_HELP_MSG_APPEND,
ApiBase::PARAM_HELP_MSG_INFO, ApiBase::PARAM_HELP_MSG_PER_VALUE, ApiBase::PARAM_TEMPLATE_VARS,
];
return [
'Basic test' => [
[ 'test' => null ],
'test',
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
]
],
'Message mapping' => [
[ 'test' => [
EnumDef::PARAM_DEPRECATED_VALUES => [
'a' => true,
'b' => 'bbb',
'c' => [ 'ccc', 'p1', 'p2' ],
'd' => Message::newFromKey( 'ddd' )->plaintextParams( 'p1', 'p2' ),
],
] ],
'test',
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [
DataMessageValue::new( 'bbb', [], 'bogus', [ '💩' => 'back-compat' ] ),
DataMessageValue::new( 'ccc', [], 'bogus', [ '💩' => 'back-compat' ] )
->params( 'p1', 'p2' ),
DataMessageValue::new( 'ddd', [], 'bogus', [ '💩' => 'back-compat' ] )
->plaintextParams( 'p1', 'p2' ),
],
]
],
'Test everything' => [
[
'xxx' => [
ParamValidator::PARAM_TYPE => 'not tested here',
ParamValidator::PARAM_ISMULTI => true
],
'test-{x}' => [
ApiBase::PARAM_TYPE => [],
ApiBase::PARAM_RANGE_ENFORCE => true,
ApiBase::PARAM_HELP_MSG => 'foo',
ApiBase::PARAM_HELP_MSG_APPEND => [],
ApiBase::PARAM_HELP_MSG_INFO => [],
ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
ApiBase::PARAM_TEMPLATE_VARS => [
'x' => 'xxx',
]
],
],
'test-{x}',
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [
MessageValue::new( 'foo' ),
],
]
],
'Bad types' => [
[ 'test' => [
ApiBase::PARAM_RANGE_ENFORCE => 1,
ApiBase::PARAM_HELP_MSG => false,
ApiBase::PARAM_HELP_MSG_APPEND => 'foo',
ApiBase::PARAM_HELP_MSG_INFO => 'bar',
ApiBase::PARAM_HELP_MSG_PER_VALUE => true,
ApiBase::PARAM_TEMPLATE_VARS => false,
] ],
'test',
[
'issues' => [
'X',
ApiBase::PARAM_RANGE_ENFORCE => 'PARAM_RANGE_ENFORCE must be boolean, got integer',
'Message specification for PARAM_HELP_MSG is not valid',
ApiBase::PARAM_HELP_MSG_APPEND => 'PARAM_HELP_MSG_APPEND must be an array, got string',
ApiBase::PARAM_HELP_MSG_INFO => 'PARAM_HELP_MSG_INFO must be an array, got string',
ApiBase::PARAM_HELP_MSG_PER_VALUE => 'PARAM_HELP_MSG_PER_VALUE must be an array, got boolean',
ApiBase::PARAM_TEMPLATE_VARS => 'PARAM_TEMPLATE_VARS must be an array, got boolean',
],
'allowedKeys' => $keys,
'messages' => [],
]
],
'PARAM_VALUE_LINKS is deprecated' => [
[ 'test' => [ ApiBase::PARAM_VALUE_LINKS => null ] ],
'test',
[
'issues' => [
'X',
ApiBase::PARAM_VALUE_LINKS => 'PARAM_VALUE_LINKS was deprecated in MediaWiki 1.35',
],
'allowedKeys' => $keys,
'messages' => [],
]
],
'PARAM_HELP_MSG (string)' => [
[ 'test' => [
ApiBase::PARAM_HELP_MSG => 'foo',
] ],
'test',
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [
MessageValue::new( 'foo' ),
],
]
],
'PARAM_HELP_MSG (array)' => [
[ 'test' => [
ApiBase::PARAM_HELP_MSG => [ 'foo', 'bar' ],
] ],
'test',
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [
MessageValue::new( 'foo', [ 'bar' ] ),
],
]
],
'PARAM_HELP_MSG (Message)' => [
[ 'test' => [
ApiBase::PARAM_HELP_MSG => Message::newFromKey( 'foo' )->numParams( 123 ),
] ],
'test',
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [
MessageValue::new( 'foo' )->numParams( 123 ),
],
]
],
'PARAM_HELP_MSG_APPEND' => [
[ 'test' => [ ApiBase::PARAM_HELP_MSG_APPEND => [
'foo',
false,
[ 'bar', 'p1', 'p2' ],
Message::newFromKey( 'baz' )->numParams( 123 ),
] ] ],
'test',
[
'issues' => [
'X',
'Message specification for PARAM_HELP_MSG_APPEND[1] is not valid',
],
'allowedKeys' => $keys,
'messages' => [
MessageValue::new( 'foo' ),
MessageValue::new( 'bar', [ 'p1', 'p2' ] ),
MessageValue::new( 'baz' )->numParams( 123 ),
],
]
],
'PARAM_HELP_MSG_INFO' => [
[ 'test' => [ ApiBase::PARAM_HELP_MSG_INFO => [
'foo',
[ false ],
[ 'foo' ],
[ 'bar', 'p1', 'p2' ],
] ] ],
'test',
[
'issues' => [
'X',
'PARAM_HELP_MSG_INFO[0] must be an array, got string',
'PARAM_HELP_MSG_INFO[1][0] must be a string, got boolean',
],
'allowedKeys' => $keys,
'messages' => [
MessageValue::new( 'apihelp-query+allpages-paraminfo-foo' ),
MessageValue::new( 'apihelp-query+allpages-paraminfo-bar', [ 'p1', 'p2' ] ),
],
]
],
'PARAM_HELP_MSG_PER_VALUE for non-array type' => [
[ 'test' => [
ParamValidator::PARAM_TYPE => 'namespace',
ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
] ],
'test',
[
'issues' => [
'X',
ApiBase::PARAM_HELP_MSG_PER_VALUE
=> 'PARAM_HELP_MSG_PER_VALUE can only be used with PARAM_TYPE as an array',
],
'allowedKeys' => $keys,
'messages' => [],
]
],
'PARAM_HELP_MSG_PER_VALUE' => [
[ 'test' => [
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd' ],
ApiBase::PARAM_HELP_MSG_PER_VALUE => [
'a' => null,
'b' => 'bbb',
'c' => [ 'ccc', 'p1', 'p2' ],
'd' => Message::newFromKey( 'ddd' )->numParams( 123 ),
'e' => 'eee',
],
] ],
'test',
[
'issues' => [
'X',
'Message specification for PARAM_HELP_MSG_PER_VALUE[a] is not valid',
'PARAM_HELP_MSG_PER_VALUE contains "e", which is not in PARAM_TYPE.',
],
'allowedKeys' => $keys,
'messages' => [
MessageValue::new( 'bbb' ),
MessageValue::new( 'ccc', [ 'p1', 'p2' ] ),
MessageValue::new( 'ddd' )->numParams( 123 ),
MessageValue::new( 'eee' ),
],
]
],
'Template-style parameter name without PARAM_TEMPLATE_VARS' => [
[ 'test{x}' => null ],
'test{x}',
[
'issues' => [
'X',
"Parameter name may not contain '{' or '}' without PARAM_TEMPLATE_VARS",
],
'allowedKeys' => $keys,
'messages' => [],
]
],
'PARAM_TEMPLATE_VARS cannot be empty' => [
[ 'test{x}' => [
ApiBase::PARAM_TEMPLATE_VARS => [],
] ],
'test{x}',
[
'issues' => [
'X',
ApiBase::PARAM_TEMPLATE_VARS => 'PARAM_TEMPLATE_VARS cannot be the empty array',
],
'allowedKeys' => $keys,
'messages' => [],
]
],
'PARAM_TEMPLATE_VARS, ok' => [
[
'ok' => [
ParamValidator::PARAM_ISMULTI => true,
],
'ok-templated-{x}' => [
ParamValidator::PARAM_ISMULTI => true,
ApiBase::PARAM_TEMPLATE_VARS => [
'x' => 'ok',
],
],
'test-{a}-{b}' => [
ApiBase::PARAM_TEMPLATE_VARS => [
'a' => 'ok',
'b' => 'ok-templated-{x}',
],
],
],
'test-{a}-{b}',
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
]
],
'PARAM_TEMPLATE_VARS simple errors' => [
[
'ok' => [
ParamValidator::PARAM_ISMULTI => true,
],
'not-multi' => false,
'test-{a}-{b}-{c}' => [
ApiBase::PARAM_TEMPLATE_VARS => [
'{x}' => 'ok',
'not-in-name' => 'ok',
'a' => false,
'b' => 'missing',
'c' => 'not-multi',
],
],
],
'test-{a}-{b}-{c}',
[
'issues' => [
'X',
"PARAM_TEMPLATE_VARS keys may not contain '{' or '}', got \"{x}\"",
'Parameter name must contain PARAM_TEMPLATE_VARS key {not-in-name}',
'PARAM_TEMPLATE_VARS[a] has invalid target type boolean',
'PARAM_TEMPLATE_VARS[b] target parameter "missing" does not exist',
'PARAM_TEMPLATE_VARS[c] target parameter "not-multi" must have PARAM_ISMULTI = true',
],
'allowedKeys' => $keys,
'messages' => [],
]
],
'PARAM_TEMPLATE_VARS no recursion' => [
[
'test-{a}' => [
ParamValidator::PARAM_ISMULTI => true,
ApiBase::PARAM_TEMPLATE_VARS => [
'a' => 'test-{a}',
],
],
],
'test-{a}',
[
'issues' => [
'X',
'PARAM_TEMPLATE_VARS[a] cannot target the parameter itself'
],
'allowedKeys' => $keys,
'messages' => [],
]
],
'PARAM_TEMPLATE_VARS targeting another template, target must be a subset' => [
[
'ok1' => [ ParamValidator::PARAM_ISMULTI => true ],
'ok2' => [ ParamValidator::PARAM_ISMULTI => true ],
'test1-{a}' => [
ApiBase::PARAM_TEMPLATE_VARS => [
'a' => 'test2-{a}',
],
],
'test2-{a}' => [
ParamValidator::PARAM_ISMULTI => true,
ApiBase::PARAM_TEMPLATE_VARS => [
'a' => 'ok2',
],
],
],
'test1-{a}',
[
'issues' => [
'X',
'PARAM_TEMPLATE_VARS[a]: Target\'s PARAM_TEMPLATE_VARS must be a subset of the original',
],
'allowedKeys' => $keys,
'messages' => [],
]
],
];
}
/**
* @dataProvider provideGetValue
* @param string|null $data Request value

View file

@ -7,6 +7,7 @@ use ApiModuleManager;
use MockApi;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef\EnumDef;
use Wikimedia\ParamValidator\TypeDef\TypeDefTestCase;
use Wikimedia\ParamValidator\ValidationException;
use Wikimedia\TestingAccessWrapper;
@ -119,6 +120,85 @@ class SubmoduleDefTest extends TypeDefTestCase {
];
}
public function provideCheckSettings() {
$opts = [
'module' => $this->mockApi(),
];
$keys = [
'Y', EnumDef::PARAM_DEPRECATED_VALUES,
SubmoduleDef::PARAM_SUBMODULE_MAP, SubmoduleDef::PARAM_SUBMODULE_PARAM_PREFIX
];
return [
'Basic test' => [
[],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
$opts
],
'Test with everything' => [
[
SubmoduleDef::PARAM_SUBMODULE_MAP => [
'foo' => 'testmod+mod1', 'bar' => 'testmod+mod2'
],
SubmoduleDef::PARAM_SUBMODULE_PARAM_PREFIX => 'g',
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
$opts
],
'Bad types' => [
[
SubmoduleDef::PARAM_SUBMODULE_MAP => false,
SubmoduleDef::PARAM_SUBMODULE_PARAM_PREFIX => true,
],
self::STDRET,
[
'issues' => [
'X',
SubmoduleDef::PARAM_SUBMODULE_MAP => 'PARAM_SUBMODULE_MAP must be an array, got boolean',
SubmoduleDef::PARAM_SUBMODULE_PARAM_PREFIX
=> 'PARAM_SUBMODULE_PARAM_PREFIX must be a string, got boolean',
],
'allowedKeys' => $keys,
'messages' => [],
],
$opts
],
'Bad values in map' => [
[
SubmoduleDef::PARAM_SUBMODULE_MAP => [
'a' => 'testmod+mod1',
'b' => false,
'c' => null,
'd' => 'testmod+mod7',
'r' => 'testmod+recurse+recurse',
],
],
self::STDRET,
[
'issues' => [
'X',
'Values for PARAM_SUBMODULE_MAP must be strings, but value for "b" is boolean',
'Values for PARAM_SUBMODULE_MAP must be strings, but value for "c" is NULL',
'PARAM_SUBMODULE_MAP contains "testmod+mod7", which is not a valid module path',
],
'allowedKeys' => $keys,
'messages' => [],
],
$opts
],
];
}
public function provideGetEnumValues() {
$opts = [
'module' => $this->mockApi(),

View file

@ -162,6 +162,293 @@ class ParamValidatorTest extends \PHPUnit\Framework\TestCase {
];
}
/** @dataProvider provideCheckSettings */
public function testCheckSettings( $settings, array $expect ) : void {
$callbacks = new SimpleCallbacks( [] );
$mb = $this->getMockBuilder( TypeDef::class )
->setConstructorArgs( [ $callbacks ] )
->setMethods( [ 'checkSettings' ] );
$mock1 = $mb->getMockForAbstractClass();
$mock1->method( 'checkSettings' )->willReturnCallback(
function ( string $name, $settings, array $options, array $ret ) {
$ret['allowedKeys'][] = 'XXX-test';
if ( isset( $settings['XXX-test'] ) ) {
$ret['issues']['XXX-test'] = 'XXX-test was ' . $settings['XXX-test'];
}
return $ret;
}
);
$mock2 = $mb->getMockForAbstractClass();
$mock2->method( 'checkSettings' )->willReturnCallback(
function ( string $name, $settings, array $options, array $ret ) {
return $ret;
}
);
$validator = new ParamValidator(
$callbacks,
new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
[ 'typeDefs' => [ 'foo' => $mock1, 'NULL' => $mock2 ] + ParamValidator::$STANDARD_TYPES ]
);
$this->assertEquals( $expect, $validator->checkSettings( 'dummy', $settings, [] ) );
}
public function provideCheckSettings() : array {
$normalKeys = [
ParamValidator::PARAM_TYPE, ParamValidator::PARAM_DEFAULT, ParamValidator::PARAM_REQUIRED,
ParamValidator::PARAM_ISMULTI, ParamValidator::PARAM_SENSITIVE, ParamValidator::PARAM_DEPRECATED,
ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES,
];
$multiKeys = array_merge( $normalKeys, [
ParamValidator::PARAM_ISMULTI_LIMIT1, ParamValidator::PARAM_ISMULTI_LIMIT2,
ParamValidator::PARAM_ALL, ParamValidator::PARAM_ALLOW_DUPLICATES
] );
$multiEnumKeys = array_merge( $multiKeys, [ TypeDef\EnumDef::PARAM_DEPRECATED_VALUES ] );
return [
'Basic test' => [
null,
[
'issues' => [],
'allowedKeys' => $normalKeys,
'messages' => [],
],
],
'Basic multi-value' => [
[
ParamValidator::PARAM_ISMULTI => true,
],
[
'issues' => [],
'allowedKeys' => $multiKeys,
'messages' => [],
],
],
'Test with everything' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd' ],
ParamValidator::PARAM_DEFAULT => 'a|b',
ParamValidator::PARAM_REQUIRED => true,
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_SENSITIVE => false,
ParamValidator::PARAM_DEPRECATED => false,
ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES => true,
ParamValidator::PARAM_ISMULTI_LIMIT1 => 10,
ParamValidator::PARAM_ISMULTI_LIMIT2 => 20,
ParamValidator::PARAM_ALL => 'all',
ParamValidator::PARAM_ALLOW_DUPLICATES => true,
],
[
'issues' => [],
'allowedKeys' => $multiEnumKeys,
'messages' => [],
],
],
'Lots of bad types' => [
[
ParamValidator::PARAM_TYPE => false,
ParamValidator::PARAM_REQUIRED => 1,
ParamValidator::PARAM_ISMULTI => 1,
ParamValidator::PARAM_ISMULTI_LIMIT1 => '10',
ParamValidator::PARAM_ISMULTI_LIMIT2 => '100',
ParamValidator::PARAM_ALL => [],
ParamValidator::PARAM_ALLOW_DUPLICATES => 1,
ParamValidator::PARAM_SENSITIVE => 1,
ParamValidator::PARAM_DEPRECATED => 1,
ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES => 1,
],
[
'issues' => [
ParamValidator::PARAM_TYPE => 'PARAM_TYPE must be a string or array, got boolean',
ParamValidator::PARAM_REQUIRED => 'PARAM_REQUIRED must be boolean, got integer',
ParamValidator::PARAM_ISMULTI => 'PARAM_ISMULTI must be boolean, got integer',
ParamValidator::PARAM_ISMULTI_LIMIT1 => 'PARAM_ISMULTI_LIMIT1 must be an integer, got string',
ParamValidator::PARAM_ISMULTI_LIMIT2 => 'PARAM_ISMULTI_LIMIT2 must be an integer, got string',
ParamValidator::PARAM_ALL => 'PARAM_ALL must be a string or boolean, got array',
ParamValidator::PARAM_ALLOW_DUPLICATES
=> 'PARAM_ALLOW_DUPLICATES must be boolean, got integer',
ParamValidator::PARAM_SENSITIVE => 'PARAM_SENSITIVE must be boolean, got integer',
ParamValidator::PARAM_DEPRECATED => 'PARAM_DEPRECATED must be boolean, got integer',
ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES
=> 'PARAM_IGNORE_UNRECOGNIZED_VALUES must be boolean, got integer',
],
'allowedKeys' => $multiKeys,
'messages' => [],
],
],
'Multi-value stuff is ignored without ISMULTI' => [
[
ParamValidator::PARAM_ISMULTI_LIMIT1 => '10',
ParamValidator::PARAM_ISMULTI_LIMIT2 => '100',
ParamValidator::PARAM_ALL => [],
ParamValidator::PARAM_ALLOW_DUPLICATES => 1,
],
[
'issues' => [],
'allowedKeys' => $normalKeys,
'messages' => [],
],
],
'PARAM_TYPE is not registered' => [
[ ParamValidator::PARAM_TYPE => 'xyz' ],
[
'issues' => [
ParamValidator::PARAM_TYPE => 'Unknown/unregistered PARAM_TYPE "xyz"',
],
'allowedKeys' => $normalKeys,
'messages' => [],
],
],
'PARAM_DEFAULT value doesn\'t validate' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd' ],
ParamValidator::PARAM_DEFAULT => 'a|b',
],
[
'issues' => [
ParamValidator::PARAM_DEFAULT => 'Value for PARAM_DEFAULT does not validate (code badvalue)',
],
'allowedKeys' => array_merge( $normalKeys, [ TypeDef\EnumDef::PARAM_DEPRECATED_VALUES ] ),
'messages' => [],
],
],
'PARAM_ISMULTI_LIMIT1 out of range' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ISMULTI_LIMIT1 => 0,
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI_LIMIT1 => 'PARAM_ISMULTI_LIMIT1 must be greater than 0, got 0',
],
'allowedKeys' => $multiKeys,
'messages' => [],
],
],
'PARAM_ISMULTI_LIMIT2 out of range' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ISMULTI_LIMIT1 => 100,
ParamValidator::PARAM_ISMULTI_LIMIT2 => 10,
],
[
'issues' => [
// phpcs:ignore Generic.Files.LineLength.TooLong
ParamValidator::PARAM_ISMULTI_LIMIT2 => 'PARAM_ISMULTI_LIMIT2 must be greater than or equal to PARAM_ISMULTI_LIMIT1, but 10 < 100',
],
'allowedKeys' => $multiKeys,
'messages' => [],
],
],
'PARAM_ISMULTI_LIMIT1 = LIMIT2 is ok' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ISMULTI_LIMIT1 => 10,
ParamValidator::PARAM_ISMULTI_LIMIT2 => 10,
],
[
'issues' => [],
'allowedKeys' => $multiKeys,
'messages' => [],
],
],
'PARAM_ALL false is ok with non-enumerated type' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ALL => false,
],
[
'issues' => [],
'allowedKeys' => $multiKeys,
'messages' => [],
],
],
'PARAM_ALL true is not ok with non-enumerated type' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ALL => true,
],
[
'issues' => [
ParamValidator::PARAM_ALL => 'PARAM_ALL cannot be used with non-enumerated types',
],
'allowedKeys' => $multiKeys,
'messages' => [],
],
],
'PARAM_ALL string is not ok with non-enumerated type' => [
[
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ALL => 'all',
],
[
'issues' => [
ParamValidator::PARAM_ALL => 'PARAM_ALL cannot be used with non-enumerated types',
],
'allowedKeys' => $multiKeys,
'messages' => [],
],
],
'PARAM_ALL true value collision' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', '*' ],
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ALL => true,
],
[
'issues' => [
ParamValidator::PARAM_ALL => 'Value for PARAM_ALL conflicts with an enumerated value',
],
'allowedKeys' => $multiEnumKeys,
'messages' => [],
],
],
'PARAM_ALL string value collision' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'all' ],
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ALL => 'all',
],
[
'issues' => [
ParamValidator::PARAM_ALL => 'Value for PARAM_ALL conflicts with an enumerated value',
],
'allowedKeys' => $multiEnumKeys,
'messages' => [],
],
],
'TypeDef is called' => [
[
ParamValidator::PARAM_TYPE => 'foo',
'XXX-test' => '!!!',
],
[
'issues' => [
'XXX-test' => 'XXX-test was !!!',
],
'allowedKeys' => array_merge( $normalKeys, [ 'XXX-test' ] ),
'messages' => [],
],
],
];
}
public function testCheckSettings_noEnum() : void {
$callbacks = new SimpleCallbacks( [] );
$validator = new ParamValidator(
$callbacks,
new ObjectFactory( $this->getMockForAbstractClass( ContainerInterface::class ) ),
[ 'typeDefs' => [] ]
);
$this->assertEquals(
[ ParamValidator::PARAM_TYPE => 'Unknown/unregistered PARAM_TYPE "enum"' ],
$validator->checkSettings( 'dummy', [ ParamValidator::PARAM_TYPE => [ 'xyz' ] ], [] )['issues']
);
}
/** @dataProvider provideExplodeMultiValue */
public function testExplodeMultiValue( $value, $limit, $expect ) {
$this->assertSame( $expect, ParamValidator::explodeMultiValue( $value, $limit ) );

View file

@ -68,6 +68,69 @@ class EnumDefTest extends TypeDefTestCase {
];
}
public function provideCheckSettings() {
return [
'Basic test' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd', 'e' ],
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => [ 'Y', EnumDef::PARAM_DEPRECATED_VALUES ],
'messages' => [],
],
],
'Bad type for PARAM_DEPRECATED_VALUES' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd', 'e' ],
EnumDef::PARAM_DEPRECATED_VALUES => false,
],
self::STDRET,
[
'issues' => [
'X',
EnumDef::PARAM_DEPRECATED_VALUES => 'PARAM_DEPRECATED_VALUES must be an array, got boolean',
],
'allowedKeys' => [ 'Y', EnumDef::PARAM_DEPRECATED_VALUES ],
'messages' => [],
],
],
'PARAM_DEPRECATED_VALUES value errors' => [
[
ParamValidator::PARAM_TYPE => [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 0, '1' ],
EnumDef::PARAM_DEPRECATED_VALUES => [
'b' => null,
'c' => false,
'd' => true,
'e' => MessageValue::new( 'e' ),
'f' => 'f',
'g' => $this,
0 => true,
1 => true,
'x' => null,
],
],
self::STDRET,
[
'issues' => [
'X',
// phpcs:disable Generic.Files.LineLength
'Values in PARAM_DEPRECATED_VALUES must be null, true, or MessageValue, but value for "c" is false',
'Values in PARAM_DEPRECATED_VALUES must be null, true, or MessageValue, but value for "f" is string',
'Values in PARAM_DEPRECATED_VALUES must be null, true, or MessageValue, but value for "g" is ' . static::class,
// phpcs:enable
'PARAM_DEPRECATED_VALUES contains "x", which is not one of the enumerated values',
],
'allowedKeys' => [ 'Y', EnumDef::PARAM_DEPRECATED_VALUES ],
'messages' => [
MessageValue::new( 'e' ),
],
],
],
];
}
public function provideGetEnumValues() {
return [
'Basic test' => [

View file

@ -88,6 +88,59 @@ class FloatDefTest extends TypeDefTestCase {
];
}
public function provideCheckSettings() {
$keys = [
'Y', FloatDef::PARAM_IGNORE_RANGE,
FloatDef::PARAM_MIN, FloatDef::PARAM_MAX, FloatDef::PARAM_MAX2
];
return [
'Basic test' => [
[],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with everything' => [
[
FloatDef::PARAM_IGNORE_RANGE => true,
FloatDef::PARAM_MIN => -100.0,
FloatDef::PARAM_MAX => -90.0,
FloatDef::PARAM_MAX2 => -80.0,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Bad types' => [
[
FloatDef::PARAM_IGNORE_RANGE => 1,
FloatDef::PARAM_MIN => 1,
FloatDef::PARAM_MAX => '2',
FloatDef::PARAM_MAX2 => '3',
],
self::STDRET,
[
'issues' => [
'X',
FloatDef::PARAM_IGNORE_RANGE => 'PARAM_IGNORE_RANGE must be boolean, got integer',
FloatDef::PARAM_MIN => 'PARAM_MIN must be double, got integer',
FloatDef::PARAM_MAX => 'PARAM_MAX must be double, got string',
FloatDef::PARAM_MAX2 => 'PARAM_MAX2 must be double, got string',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
];
}
public function provideStringifyValue() {
$digits = defined( 'PHP_FLOAT_DIG' ) ? PHP_FLOAT_DIG : 15;

View file

@ -134,6 +134,127 @@ class IntegerDefTest extends TypeDefTestCase {
];
}
public function provideCheckSettings() {
$keys = [
'Y', IntegerDef::PARAM_IGNORE_RANGE,
IntegerDef::PARAM_MIN, IntegerDef::PARAM_MAX, IntegerDef::PARAM_MAX2
];
return [
'Basic test' => [
[],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with everything' => [
[
IntegerDef::PARAM_IGNORE_RANGE => true,
IntegerDef::PARAM_MIN => -100,
IntegerDef::PARAM_MAX => -90,
IntegerDef::PARAM_MAX2 => -80,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Bad types' => [
[
IntegerDef::PARAM_IGNORE_RANGE => 1,
IntegerDef::PARAM_MIN => 1.0,
IntegerDef::PARAM_MAX => '2',
IntegerDef::PARAM_MAX2 => '3',
],
self::STDRET,
[
'issues' => [
'X',
IntegerDef::PARAM_IGNORE_RANGE => 'PARAM_IGNORE_RANGE must be boolean, got integer',
IntegerDef::PARAM_MIN => 'PARAM_MIN must be integer, got double',
IntegerDef::PARAM_MAX => 'PARAM_MAX must be integer, got string',
IntegerDef::PARAM_MAX2 => 'PARAM_MAX2 must be integer, got string',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Min == max' => [
[
IntegerDef::PARAM_MIN => 1,
IntegerDef::PARAM_MAX => 1,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Min > max' => [
[
IntegerDef::PARAM_MIN => 2,
IntegerDef::PARAM_MAX => 1,
],
self::STDRET,
[
'issues' => [
'X',
'PARAM_MIN must be less than or equal to PARAM_MAX, but 2 > 1',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Max2 without max' => [
[
IntegerDef::PARAM_MAX2 => 1,
],
self::STDRET,
[
'issues' => [
'X',
'PARAM_MAX2 cannot be used without PARAM_MAX',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Max2 == max' => [
[
IntegerDef::PARAM_MAX => 1,
IntegerDef::PARAM_MAX2 => 1,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Max2 < max' => [
[
IntegerDef::PARAM_MAX => -10,
IntegerDef::PARAM_MAX2 => -11,
],
self::STDRET,
[
'issues' => [
'X',
'PARAM_MAX2 must be greater than or equal to PARAM_MAX, but -11 < -10',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
];
}
public function provideGetInfo() {
return [
'Basic' => [

View file

@ -57,6 +57,115 @@ class LimitDefTest extends TypeDefTestCase {
];
}
public function provideCheckSettings() {
$keys = [
'Y', IntegerDef::PARAM_IGNORE_RANGE,
IntegerDef::PARAM_MIN, IntegerDef::PARAM_MAX, IntegerDef::PARAM_MAX2
];
return [
'Basic test' => [
[
LimitDef::PARAM_MAX => 10,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with everything' => [
[
LimitDef::PARAM_IGNORE_RANGE => true,
LimitDef::PARAM_MIN => 0,
LimitDef::PARAM_MAX => 10,
LimitDef::PARAM_MAX2 => 100,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_ISMULTI not allowed' => [
[
ParamValidator::PARAM_ISMULTI => true,
LimitDef::PARAM_MAX => 10,
],
self::STDRET,
[
'issues' => [
'X',
ParamValidator::PARAM_ISMULTI => 'PARAM_ISMULTI cannot be used for limit-type parameters',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_ISMULTI not allowed, but another ISMULTI issue was already logged' => [
[
ParamValidator::PARAM_ISMULTI => true,
LimitDef::PARAM_MAX => 10,
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI => 'XXX',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_MIN == 0' => [
[
LimitDef::PARAM_MIN => 0,
LimitDef::PARAM_MAX => 2,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_MIN < 0' => [
[
LimitDef::PARAM_MIN => -1,
LimitDef::PARAM_MAX => 2,
],
self::STDRET,
[
'issues' => [
'X',
'PARAM_MIN must be greater than or equal to 0',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'PARAM_MAX is required' => [
[],
self::STDRET,
[
'issues' => [
'X',
'PARAM_MAX must be set',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
];
}
public function provideGetInfo() {
return [
'Basic' => [

View file

@ -20,4 +20,45 @@ class PasswordDefTest extends StringDefTest {
];
}
public function provideCheckSettings() {
$keys = [ 'Y', StringDef::PARAM_MAX_BYTES, StringDef::PARAM_MAX_CHARS ];
yield from parent::provideCheckSettings();
yield 'PARAM_SENSITIVE cannot be false' => [
[
ParamValidator::PARAM_SENSITIVE => false,
],
self::STDRET,
[
'issues' => [
'X',
ParamValidator::PARAM_SENSITIVE
=> 'Cannot set PARAM_SENSITIVE to false for password-type parameters',
],
'allowedKeys' => $keys,
'messages' => [],
],
];
yield 'PARAM_SENSITIVE cannot be false, but another PARAM_SENSITIVE issue was already logged' => [
[
ParamValidator::PARAM_SENSITIVE => false,
],
[
'issues' => [
ParamValidator::PARAM_SENSITIVE => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [
ParamValidator::PARAM_SENSITIVE => 'XXX',
],
'allowedKeys' => $keys,
'messages' => [],
],
];
}
}

View file

@ -28,6 +28,94 @@ class PresenceBooleanDefTest extends TypeDefTestCase {
];
}
public function provideCheckSettings() {
return [
'Basic test' => [
[],
self::STDRET,
self::STDRET,
],
'PARAM_ISMULTI not allowed' => [
[
ParamValidator::PARAM_ISMULTI => true,
],
self::STDRET,
[
'issues' => [
'X',
ParamValidator::PARAM_ISMULTI
=> 'PARAM_ISMULTI cannot be used for presence-boolean-type parameters',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
'PARAM_ISMULTI not allowed, but another ISMULTI issue was already logged' => [
[
ParamValidator::PARAM_ISMULTI => true,
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
'PARAM_DEFAULT can be false' => [
[ ParamValidator::PARAM_DEFAULT => false ],
self::STDRET,
self::STDRET,
],
'PARAM_DEFAULT can be null' => [
[ ParamValidator::PARAM_DEFAULT => null ],
self::STDRET,
self::STDRET,
],
'PARAM_DEFAULT cannot be true' => [
[
ParamValidator::PARAM_DEFAULT => true,
],
self::STDRET,
[
'issues' => [
'X',
ParamValidator::PARAM_DEFAULT
=> 'Default for presence-boolean-type parameters must be false or null',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
'PARAM_DEFAULT invalid, but another DEFAULT issue was already logged' => [
[
ParamValidator::PARAM_DEFAULT => true,
],
[
'issues' => [
ParamValidator::PARAM_DEFAULT => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [
ParamValidator::PARAM_DEFAULT => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
];
}
public function provideGetInfo() {
return [
'Basic test' => [

View file

@ -92,6 +92,113 @@ class StringDefTest extends TypeDefTestCase {
];
}
public function provideCheckSettings() {
$keys = [ 'Y', StringDef::PARAM_MAX_BYTES, StringDef::PARAM_MAX_CHARS ];
return [
'Basic test' => [
[],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with everything' => [
[
StringDef::PARAM_MAX_BYTES => 255,
StringDef::PARAM_MAX_CHARS => 100,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Bad types' => [
[
StringDef::PARAM_MAX_BYTES => '255',
StringDef::PARAM_MAX_CHARS => 100.0,
],
self::STDRET,
[
'issues' => [
'X',
StringDef::PARAM_MAX_BYTES => 'PARAM_MAX_BYTES must be an integer, got string',
StringDef::PARAM_MAX_CHARS => 'PARAM_MAX_CHARS must be an integer, got double',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Out of range' => [
[
StringDef::PARAM_MAX_BYTES => -1,
StringDef::PARAM_MAX_CHARS => -1,
],
self::STDRET,
[
'issues' => [
'X',
StringDef::PARAM_MAX_BYTES => 'PARAM_MAX_BYTES must be greater than or equal to 0',
StringDef::PARAM_MAX_CHARS => 'PARAM_MAX_CHARS must be greater than or equal to 0',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Zero not allowed when required and !allowEmptyWhenRequired' => [
[
ParamValidator::PARAM_REQUIRED => true,
StringDef::PARAM_MAX_BYTES => 0,
StringDef::PARAM_MAX_CHARS => 0,
],
self::STDRET,
[
'issues' => [
'X',
// phpcs:ignore Generic.Files.LineLength
'PARAM_REQUIRED is set, allowEmptyWhenRequired is not set, and PARAM_MAX_BYTES is 0. That\'s impossible to satisfy.',
// phpcs:ignore Generic.Files.LineLength
'PARAM_REQUIRED is set, allowEmptyWhenRequired is not set, and PARAM_MAX_CHARS is 0. That\'s impossible to satisfy.',
],
'allowedKeys' => $keys,
'messages' => [],
],
[ 'allowEmptyWhenRequired' => false ],
],
'Zero allowed when not required' => [
[
StringDef::PARAM_MAX_BYTES => 0,
StringDef::PARAM_MAX_CHARS => 0,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
[ 'allowEmptyWhenRequired' => false ],
],
'Zero allowed when allowEmptyWhenRequired' => [
[
ParamValidator::PARAM_REQUIRED => true,
StringDef::PARAM_MAX_BYTES => 0,
StringDef::PARAM_MAX_CHARS => 0,
],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
[ 'allowEmptyWhenRequired' => true ],
],
];
}
public function provideGetInfo() {
return [
'Basic test' => [

View file

@ -23,6 +23,34 @@ class TimestampDefTest extends TypeDefTestCase {
return new static::$testClass( $callbacks, $options );
}
/** @dataProvider provideConstructorOptions */
public function testConstructorOptions( array $options, $ok ) : void {
if ( $ok ) {
$this->assertTrue( true ); // dummy
} else {
$this->expectException( \InvalidArgumentException::class );
}
$this->getInstance( new SimpleCallbacks( [] ), $options );
}
public function provideConstructorOptions() : array {
return [
'Basic test' => [ [], true ],
'Default format ConvertibleTimestamp' => [ [ 'defaultFormat' => 'ConvertibleTimestamp' ], true ],
'Default format DateTime' => [ [ 'defaultFormat' => 'DateTime' ], true ],
'Default format TS_ISO_8601' => [ [ 'defaultFormat' => TS_ISO_8601 ], true ],
'Default format invalid (string)' => [ [ 'defaultFormat' => 'foobar' ], false ],
'Default format invalid (int)' => [ [ 'defaultFormat' => 1000 ], false ],
'Stringify format ConvertibleTimestamp' => [
[ 'stringifyFormat' => 'ConvertibleTimestamp' ], false
],
'Stringify format DateTime' => [ [ 'stringifyFormat' => 'DateTime' ], false ],
'Stringify format TS_ISO_8601' => [ [ 'stringifyFormat' => TS_ISO_8601 ], true ],
'Stringify format invalid (string)' => [ [ 'stringifyFormat' => 'foobar' ], false ],
'Stringify format invalid (int)' => [ [ 'stringifyFormat' => 1000 ], false ],
];
}
/** @dataProvider provideValidate */
public function testValidate(
$value, $expect, array $settings = [], array $options = [], array $expectConds = []
@ -82,6 +110,73 @@ class TimestampDefTest extends TypeDefTestCase {
];
}
public function provideCheckSettings() {
$keys = [ 'Y', TimestampDef::PARAM_TIMESTAMP_FORMAT ];
return [
'Basic test' => [
[],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with format ConvertibleTimestamp' => [
[ TimestampDef::PARAM_TIMESTAMP_FORMAT => 'ConvertibleTimestamp' ],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with format DateTime' => [
[ TimestampDef::PARAM_TIMESTAMP_FORMAT => 'DateTime' ],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with format TS_ISO_8601' => [
[ TimestampDef::PARAM_TIMESTAMP_FORMAT => TS_ISO_8601 ],
self::STDRET,
[
'issues' => [ 'X' ],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with invalid format (string)' => [
[ TimestampDef::PARAM_TIMESTAMP_FORMAT => 'foobar' ],
self::STDRET,
[
'issues' => [
'X',
TimestampDef::PARAM_TIMESTAMP_FORMAT => 'Value for PARAM_TIMESTAMP_FORMAT is not valid',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
'Test with invalid format (int)' => [
[ TimestampDef::PARAM_TIMESTAMP_FORMAT => 1000 ],
self::STDRET,
[
'issues' => [
'X',
TimestampDef::PARAM_TIMESTAMP_FORMAT => 'Value for PARAM_TIMESTAMP_FORMAT is not valid',
],
'allowedKeys' => $keys,
'messages' => [],
],
],
];
}
public function provideStringifyValue() {
$specific = new ConvertibleTimestamp( '20180203040506' );

View file

@ -15,6 +15,9 @@ use Wikimedia\ParamValidator\ValidationException;
*/
abstract class TypeDefTestCase extends \PHPUnit\Framework\TestCase {
/** Standard "$ret" array for provideCheckSettings */
protected const STDRET = [ 'issues' => [ 'X' ], 'allowedKeys' => [ 'Y' ], 'messages' => [] ];
/** @var string|null TypeDef class name being tested */
protected static $testClass = null;
@ -116,6 +119,32 @@ abstract class TypeDefTestCase extends \PHPUnit\Framework\TestCase {
];
}
/**
* @dataProvider provideCheckSettings
* @param array $settings
* @param array $ret Input $ret array
* @param array $expect
* @param array $options Options array
*/
public function testCheckSettings(
array $settings,
array $ret,
array $expect,
array $options = []
) : void {
$typeDef = $this->getInstance( new SimpleCallbacks( [] ), $options );
$this->assertEquals( $expect, $typeDef->checkSettings( 'test', $settings, $options, $ret ) );
}
/**
* @return array|Iterable
*/
public function provideCheckSettings() {
return [
'Basic test' => [ [], self::STDRET, self::STDRET ],
];
}
/**
* @dataProvider provideGetEnumValues
* @param array $settings

View file

@ -173,6 +173,76 @@ class UploadDefTest extends TypeDefTestCase {
$typeDef->validate( 'test', $value, [], [] );
}
public function provideCheckSettings() {
return [
'Basic test' => [
[],
self::STDRET,
self::STDRET,
],
'PARAM_ISMULTI not allowed' => [
[
ParamValidator::PARAM_ISMULTI => true,
],
self::STDRET,
[
'issues' => [
'X',
ParamValidator::PARAM_ISMULTI
=> 'PARAM_ISMULTI cannot be used for upload-type parameters',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
'PARAM_ISMULTI not allowed, but another ISMULTI issue was already logged' => [
[
ParamValidator::PARAM_ISMULTI => true,
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [
ParamValidator::PARAM_ISMULTI => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
'PARAM_DEFAULT can be null' => [
[ ParamValidator::PARAM_DEFAULT => null ],
self::STDRET,
self::STDRET,
],
'PARAM_DEFAULT is otherwise not allowed' => [
[
ParamValidator::PARAM_DEFAULT => true,
],
[
'issues' => [
'X',
ParamValidator::PARAM_DEFAULT => 'XXX',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
[
'issues' => [
'X',
ParamValidator::PARAM_DEFAULT => 'Cannot specify a default for upload-type parameters',
],
'allowedKeys' => [ 'Y' ],
'messages' => [],
],
],
];
}
public function provideStringifyValue() {
return [
'Yeah, right' => [ $this->makeUpload(), null ],

View file

@ -16,6 +16,8 @@ class TypeDefTest extends \PHPUnit\Framework\TestCase {
->getMockForAbstractClass();
$this->assertSame( [ 'foobar' ], $typeDef->normalizeSettings( [ 'foobar' ] ) );
$ret = [ 'issues' => [], 'allowedKeys' => [], 'messages' => [] ];
$this->assertSame( $ret, $typeDef->checkSettings( 'foobar', [], [], $ret ) );
$this->assertNull( $typeDef->getEnumValues( 'foobar', [], [] ) );
$this->assertSame( '123', $typeDef->stringifyValue( 'foobar', 123, [], [] ) );
}

View file

@ -1,6 +1,5 @@
<?php
use MediaWiki\MediaWikiServices;
use Wikimedia\TestingAccessWrapper;
/**
@ -27,82 +26,6 @@ class ApiStructureTest extends MediaWikiTestCase {
],
];
/**
* Values are an array, where each array value is a permitted type. A type
* can be a string, which is the name of an internal type or a
* class/interface. Or it can be an array, in which case the value must be
* an array whose elements are the types given in the array (e.g., [
* 'string', integer' ] means an array whose entries are strings and/or
* integers).
*/
private static $paramTypes = [
// ApiBase::PARAM_DFLT => as appropriate for PARAM_TYPE
ApiBase::PARAM_ISMULTI => [ 'boolean' ],
ApiBase::PARAM_TYPE => [ 'string', [ 'string' ] ],
ApiBase::PARAM_MAX => [ 'integer' ],
ApiBase::PARAM_MAX2 => [ 'integer' ],
ApiBase::PARAM_MIN => [ 'integer' ],
ApiBase::PARAM_ALLOW_DUPLICATES => [ 'boolean' ],
ApiBase::PARAM_DEPRECATED => [ 'boolean' ],
ApiBase::PARAM_REQUIRED => [ 'boolean' ],
ApiBase::PARAM_RANGE_ENFORCE => [ 'boolean' ],
ApiBase::PARAM_HELP_MSG => [ 'string', 'array', Message::class ],
ApiBase::PARAM_HELP_MSG_APPEND => [ [ 'string', 'array', Message::class ] ],
ApiBase::PARAM_HELP_MSG_INFO => [ [ 'array' ] ],
ApiBase::PARAM_VALUE_LINKS => [ [ 'string' ] ],
ApiBase::PARAM_HELP_MSG_PER_VALUE => [ [ 'string', 'array', Message::class ] ],
ApiBase::PARAM_SUBMODULE_MAP => [ [ 'string' ] ],
ApiBase::PARAM_SUBMODULE_PARAM_PREFIX => [ 'string' ],
ApiBase::PARAM_ALL => [ 'boolean', 'string' ],
ApiBase::PARAM_EXTRA_NAMESPACES => [ [ 'integer' ] ],
ApiBase::PARAM_SENSITIVE => [ 'boolean' ],
ApiBase::PARAM_DEPRECATED_VALUES => [ 'array' ],
ApiBase::PARAM_ISMULTI_LIMIT1 => [ 'integer' ],
ApiBase::PARAM_ISMULTI_LIMIT2 => [ 'integer' ],
ApiBase::PARAM_MAX_BYTES => [ 'integer' ],
ApiBase::PARAM_MAX_CHARS => [ 'integer' ],
ApiBase::PARAM_TEMPLATE_VARS => [ 'array' ],
];
// param => [ other param that must be present => required value or null ]
private static $paramRequirements = [
ApiBase::PARAM_ALLOW_DUPLICATES => [ ApiBase::PARAM_ISMULTI => true ],
ApiBase::PARAM_ALL => [ ApiBase::PARAM_ISMULTI => true ],
ApiBase::PARAM_ISMULTI_LIMIT1 => [
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_ISMULTI_LIMIT2 => null,
],
ApiBase::PARAM_ISMULTI_LIMIT2 => [
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_ISMULTI_LIMIT1 => null,
],
];
// param => type(s) allowed for this param ('array' is any array)
private static $paramAllowedTypes = [
ApiBase::PARAM_MAX => [ 'integer', 'limit' ],
ApiBase::PARAM_MAX2 => 'limit',
ApiBase::PARAM_MIN => [ 'integer', 'limit' ],
ApiBase::PARAM_RANGE_ENFORCE => 'integer',
ApiBase::PARAM_VALUE_LINKS => 'array',
ApiBase::PARAM_HELP_MSG_PER_VALUE => 'array',
ApiBase::PARAM_SUBMODULE_MAP => 'submodule',
ApiBase::PARAM_SUBMODULE_PARAM_PREFIX => 'submodule',
ApiBase::PARAM_ALL => 'array',
ApiBase::PARAM_EXTRA_NAMESPACES => 'namespace',
ApiBase::PARAM_DEPRECATED_VALUES => 'array',
ApiBase::PARAM_MAX_BYTES => [ 'NULL', 'string', 'text', 'password' ],
ApiBase::PARAM_MAX_CHARS => [ 'NULL', 'string', 'text', 'password' ],
];
private static $paramProhibitedTypes = [
ApiBase::PARAM_ISMULTI => [ 'boolean', 'limit', 'upload' ],
ApiBase::PARAM_ALL => 'namespace',
ApiBase::PARAM_SENSITIVE => 'password',
];
private static $constantNames = null;
/**
* Initialize/fetch the ApiMain instance for testing
* @return ApiMain
@ -158,62 +81,6 @@ class ApiStructureTest extends MediaWikiTestCase {
$this->checkMessage( $module->getSummaryMessage(), 'Module summary' );
$this->checkMessage( $module->getExtendedDescription(), 'Module help top text' );
// Parameters. Lots of messages in here.
$params = $module->getFinalParams( ApiBase::GET_VALUES_FOR_HELP );
$tags = [];
foreach ( $params as $name => $settings ) {
if ( !is_array( $settings ) ) {
$settings = [];
}
// Basic description message
if ( isset( $settings[ApiBase::PARAM_HELP_MSG] ) ) {
$msg = $settings[ApiBase::PARAM_HELP_MSG];
} else {
$msg = "apihelp-{$path}-param-{$name}";
}
$this->checkMessage( $msg, "Parameter $name description" );
// If param-per-value is in use, each value's message
if ( isset( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE] ) ) {
$this->assertIsArray( $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE],
"Parameter $name PARAM_HELP_MSG_PER_VALUE is array" );
$this->assertIsArray( $settings[ApiBase::PARAM_TYPE],
"Parameter $name PARAM_TYPE is array for msg-per-value mode" );
$valueMsgs = $settings[ApiBase::PARAM_HELP_MSG_PER_VALUE];
foreach ( $settings[ApiBase::PARAM_TYPE] as $value ) {
if ( isset( $valueMsgs[$value] ) ) {
$msg = $valueMsgs[$value];
} else {
$msg = "apihelp-{$path}-paramvalue-{$name}-{$value}";
}
$this->checkMessage( $msg, "Parameter $name value $value" );
}
}
// Appended messages (e.g. "disabled in miser mode")
if ( isset( $settings[ApiBase::PARAM_HELP_MSG_APPEND] ) ) {
$this->assertIsArray( $settings[ApiBase::PARAM_HELP_MSG_APPEND],
"Parameter $name PARAM_HELP_MSG_APPEND is array" );
foreach ( $settings[ApiBase::PARAM_HELP_MSG_APPEND] as $i => $msg ) {
$this->checkMessage( $msg, "Parameter $name HELP_MSG_APPEND #$i" );
}
}
// Info tags (e.g. "only usable in mode 1") are typically shared by
// several parameters, so accumulate them and test them later.
if ( !empty( $settings[ApiBase::PARAM_HELP_MSG_INFO] ) ) {
foreach ( $settings[ApiBase::PARAM_HELP_MSG_INFO] as $i ) {
$tags[array_shift( $i )] = 1;
}
}
}
// Info tags (e.g. "only usable in mode 1") accumulated above
foreach ( $tags as $tag => $dummy ) {
$this->checkMessage( "apihelp-{$path}-paraminfo-{$tag}", "HELP_MSG_INFO tag $tag" );
}
// Messages for examples.
foreach ( $module->getExamplesMessages() as $qs => $msg ) {
$this->assertStringStartsNotWith( 'api.php?', $qs,
@ -242,399 +109,72 @@ class ApiStructureTest extends MediaWikiTestCase {
}
/**
* @dataProvider provideParameterConsistency
* @dataProvider provideParameters
* @param string $path
* @param array $params
* @param string $name
*/
public function testParameterConsistency( $path ) {
public function testParameters( string $path, array $params, string $name ) : void {
$main = self::getMain();
$module = TestingAccessWrapper::newFromObject( $main->getModuleFromPath( $path ) );
$paramsPlain = $module->getFinalParams();
$paramsForHelp = $module->getFinalParams( ApiBase::GET_VALUES_FOR_HELP );
$dataName = $this->dataName();
$this->assertNotSame( '', $name, "$dataName: Name cannot be empty" );
$this->assertArrayHasKey( $name, $params, "$dataName: Sanity check" );
// avoid warnings about empty tests when no parameter needs to be checked
$this->assertTrue( true );
$ret = $main->getParamValidator()->checkSettings(
$main->getModuleFromPath( $path ), $params, $name, []
);
if ( self::$constantNames === null ) {
self::$constantNames = [];
foreach ( ( new ReflectionClass( 'ApiBase' ) )->getConstants() as $key => $val ) {
if ( substr( $key, 0, 6 ) === 'PARAM_' ) {
self::$constantNames[$val] = $key;
}
// Warn about unknown keys. Don't fail, they might be for forward- or back-compat.
if ( is_array( $params[$name] ) ) {
$keys = array_diff(
array_keys( $params[$name] ),
$ret['allowedKeys']
);
if ( $keys ) {
// Don't fail for this, for back-compat
$this->addWarning(
"$dataName: Unrecognized settings keys were used: " . implode( ', ', $keys )
);
}
}
foreach ( [ $paramsPlain, $paramsForHelp ] as $params ) {
foreach ( $params as $param => $config ) {
if ( !is_array( $config ) ) {
$config = [ ApiBase::PARAM_DFLT => $config ];
}
if ( !isset( $config[ApiBase::PARAM_TYPE] ) ) {
$config[ApiBase::PARAM_TYPE] = isset( $config[ApiBase::PARAM_DFLT] )
? gettype( $config[ApiBase::PARAM_DFLT] )
: 'NULL';
}
if ( count( $ret['issues'] ) === 1 ) {
$this->fail( "$dataName: Validation failed: " . reset( $ret['issues'] ) );
} elseif ( $ret['issues'] ) {
$this->fail( "$dataName: Validation failed:\n* " . implode( "\n* ", $ret['issues'] ) );
}
foreach ( self::$paramTypes as $key => $types ) {
if ( !isset( $config[$key] ) ) {
continue;
}
$keyName = self::$constantNames[$key];
$this->validateType( $types, $config[$key], $param, $keyName );
}
foreach ( self::$paramRequirements as $key => $required ) {
if ( !isset( $config[$key] ) ) {
continue;
}
foreach ( $required as $requireKey => $requireVal ) {
$this->assertArrayHasKey( $requireKey, $config,
"$param: When " . self::$constantNames[$key] . " is set, " .
self::$constantNames[$requireKey] . " must also be set" );
if ( $requireVal !== null ) {
$this->assertSame( $requireVal, $config[$requireKey],
"$param: When " . self::$constantNames[$key] . " is set, " .
self::$constantNames[$requireKey] . " must equal " .
var_export( $requireVal, true ) );
}
}
}
foreach ( self::$paramAllowedTypes as $key => $allowedTypes ) {
if ( !isset( $config[$key] ) ) {
continue;
}
$actualType = is_array( $config[ApiBase::PARAM_TYPE] )
? 'array' : $config[ApiBase::PARAM_TYPE];
$this->assertContains(
$actualType,
(array)$allowedTypes,
"$param: " . self::$constantNames[$key] .
" can only be used with PARAM_TYPE " .
implode( ', ', (array)$allowedTypes )
);
}
foreach ( self::$paramProhibitedTypes as $key => $prohibitedTypes ) {
if ( !isset( $config[$key] ) ) {
continue;
}
$actualType = is_array( $config[ApiBase::PARAM_TYPE] )
? 'array' : $config[ApiBase::PARAM_TYPE];
$this->assertNotContains(
$actualType,
(array)$prohibitedTypes,
"$param: " . self::$constantNames[$key] .
" cannot be used with PARAM_TYPE " .
implode( ', ', (array)$prohibitedTypes )
);
}
if ( isset( $config[ApiBase::PARAM_DFLT] ) ) {
$this->assertFalse(
isset( $config[ApiBase::PARAM_REQUIRED] ) &&
$config[ApiBase::PARAM_REQUIRED],
"$param: A required parameter cannot have a default" );
$this->validateDefault( $param, $config );
}
if ( $config[ApiBase::PARAM_TYPE] === 'limit' ) {
$this->assertTrue(
isset( $config[ApiBase::PARAM_MAX] ) &&
isset( $config[ApiBase::PARAM_MAX2] ),
"$param: PARAM_MAX and PARAM_MAX2 are required for limits"
);
$this->assertGreaterThanOrEqual(
$config[ApiBase::PARAM_MAX],
$config[ApiBase::PARAM_MAX2],
"$param: PARAM_MAX cannot be greater than PARAM_MAX2"
);
}
if (
isset( $config[ApiBase::PARAM_MIN] ) &&
isset( $config[ApiBase::PARAM_MAX] )
) {
$this->assertGreaterThanOrEqual(
$config[ApiBase::PARAM_MIN],
$config[ApiBase::PARAM_MAX],
"$param: PARAM_MIN cannot be greater than PARAM_MAX"
);
}
if ( isset( $config[ApiBase::PARAM_RANGE_ENFORCE] ) ) {
$this->assertTrue(
isset( $config[ApiBase::PARAM_MIN] ) ||
isset( $config[ApiBase::PARAM_MAX] ),
"$param: PARAM_RANGE_ENFORCE can only be set together with " .
"PARAM_MIN or PARAM_MAX"
);
}
if ( isset( $config[ApiBase::PARAM_DEPRECATED_VALUES] ) ) {
foreach ( $config[ApiBase::PARAM_DEPRECATED_VALUES] as $key => $unused ) {
$this->assertContains( $key, $config[ApiBase::PARAM_TYPE],
"$param: Deprecated value \"$key\" is not allowed, " .
"how can it be deprecated?" );
}
}
if (
isset( $config[ApiBase::PARAM_ISMULTI_LIMIT1] ) ||
isset( $config[ApiBase::PARAM_ISMULTI_LIMIT2] )
) {
$this->assertGreaterThanOrEqual( 0, $config[ApiBase::PARAM_ISMULTI_LIMIT1],
"$param: PARAM_ISMULTI_LIMIT1 cannot be negative" );
// Zero for both doesn't make sense, but you could have
// zero for non-bots
$this->assertGreaterThanOrEqual( 1, $config[ApiBase::PARAM_ISMULTI_LIMIT2],
"$param: PARAM_ISMULTI_LIMIT2 cannot be negative or zero" );
$this->assertGreaterThanOrEqual(
$config[ApiBase::PARAM_ISMULTI_LIMIT1],
$config[ApiBase::PARAM_ISMULTI_LIMIT2],
"$param: PARAM_ISMULTI limit cannot be smaller for users with " .
"apihighlimits rights" );
}
if ( isset( $config[ApiBase::PARAM_MAX_BYTES] ) ) {
$this->assertGreaterThanOrEqual( 1, $config[ApiBase::PARAM_MAX_BYTES],
"$param: PARAM_MAX_BYTES cannot be negative or zero" );
}
if ( isset( $config[ApiBase::PARAM_MAX_CHARS] ) ) {
$this->assertGreaterThanOrEqual( 1, $config[ApiBase::PARAM_MAX_CHARS],
"$param: PARAM_MAX_CHARS cannot be negative or zero" );
}
if (
isset( $config[ApiBase::PARAM_MAX_BYTES] ) &&
isset( $config[ApiBase::PARAM_MAX_CHARS] )
) {
// Length of a string in chars is always <= length in bytes,
// so PARAM_MAX_CHARS is pointless if > PARAM_MAX_BYTES
$this->assertGreaterThanOrEqual(
$config[ApiBase::PARAM_MAX_CHARS],
$config[ApiBase::PARAM_MAX_BYTES],
"$param: PARAM_MAX_BYTES cannot be less than PARAM_MAX_CHARS"
);
}
if ( isset( $config[ApiBase::PARAM_TEMPLATE_VARS] ) ) {
$this->assertNotSame( [], $config[ApiBase::PARAM_TEMPLATE_VARS],
"$param: PARAM_TEMPLATE_VARS cannot be empty" );
foreach ( $config[ApiBase::PARAM_TEMPLATE_VARS] as $key => $target ) {
$this->assertRegExp( '/^[^{}]+$/', $key,
"$param: PARAM_TEMPLATE_VARS key may not contain '{' or '}'" );
$this->assertStringContainsString( '{' . $key . '}', $param,
"$param: Name must contain PARAM_TEMPLATE_VARS key {" . $key . "}" );
$this->assertArrayHasKey( $target, $params,
"$param: PARAM_TEMPLATE_VARS target parameter '$target' does not exist" );
$config2 = $params[$target];
$this->assertTrue( !empty( $config2[ApiBase::PARAM_ISMULTI] ),
"$param: PARAM_TEMPLATE_VARS target parameter '$target' must have PARAM_ISMULTI = true" );
if ( isset( $config2[ApiBase::PARAM_TEMPLATE_VARS] ) ) {
$this->assertNotSame( $param, $target,
"$param: PARAM_TEMPLATE_VARS cannot target itself" );
$this->assertArraySubset(
$config2[ApiBase::PARAM_TEMPLATE_VARS],
$config[ApiBase::PARAM_TEMPLATE_VARS],
true,
"$param: PARAM_TEMPLATE_VARS target parameter '$target': "
. "the target's PARAM_TEMPLATE_VARS must be a subset of the original."
);
}
}
$keys = implode( '|',
array_map(
function ( $key ) {
return preg_quote( $key, '/' );
},
array_keys( $config[ApiBase::PARAM_TEMPLATE_VARS] )
)
);
$this->assertRegExp( '/^(?>[^{}]+|\{(?:' . $keys . ')\})+$/', $param,
"$param: Name may not contain '{' or '}' other than as defined by PARAM_TEMPLATE_VARS" );
} else {
$this->assertRegExp( '/^[^{}]+$/', $param,
"$param: Name may not contain '{' or '}' without PARAM_TEMPLATE_VARS" );
}
// Check message existence
$done = [];
foreach ( $ret['messages'] as $msg ) {
// We don't really care about the parameters, so do it simply
$key = $msg->getKey();
if ( !isset( $done[$key] ) ) {
$done[$key] = true;
$this->checkMessage( $key, "$dataName: Parameter" );
}
}
}
/**
* Throws if $value does not match one of the types specified in $types.
*
* @param array $types From self::$paramTypes array
* @param mixed $value Value to check
* @param string $param Name of param we're checking, for error messages
* @param string $desc Description for error messages
*/
private function validateType( $types, $value, $param, $desc ) {
if ( count( $types ) === 1 ) {
// Only one type allowed
if ( is_string( $types[0] ) ) {
$this->assertSame( $types[0], gettype( $value ), "$param: $desc type" );
} else {
// Array whose values have specified types, recurse
$this->assertIsArray( $value, "$param: $desc type" );
foreach ( $value as $subvalue ) {
$this->validateType( $types[0], $subvalue, $param, "$desc value" );
}
}
} else {
// Multiple options
foreach ( $types as $type ) {
if ( is_string( $type ) ) {
if ( class_exists( $type ) || interface_exists( $type ) ) {
if ( $value instanceof $type ) {
return;
}
} elseif ( gettype( $value ) === $type ) {
return;
}
} else {
// Array whose values have specified types, recurse
try {
$this->validateType( [ $type ], $value, $param, "$desc type" );
// Didn't throw, so we're good
return;
} catch ( Exception $unused ) {
}
}
}
// Doesn't match any of them
$this->fail( "$param: $desc has incorrect type" );
}
}
/**
* Asserts that $default is a valid default for $type.
*
* @param string $param Name of param, for error messages
* @param array $config Array of configuration options for this parameter
*/
private function validateDefault( $param, $config ) {
$type = $config[ApiBase::PARAM_TYPE];
$default = $config[ApiBase::PARAM_DFLT];
if ( !empty( $config[ApiBase::PARAM_ISMULTI] ) ) {
if ( $default === '' ) {
// The empty array is fine
return;
}
$defaults = explode( '|', $default );
$config[ApiBase::PARAM_ISMULTI] = false;
foreach ( $defaults as $defaultValue ) {
// Only allow integers in their simplest form with no leading
// or trailing characters etc.
if ( $type === 'integer' && $defaultValue === (string)(int)$defaultValue ) {
$defaultValue = (int)$defaultValue;
}
$config[ApiBase::PARAM_DFLT] = $defaultValue;
$this->validateDefault( $param, $config );
}
return;
}
switch ( $type ) {
case 'boolean':
$this->assertFalse( $default,
"$param: Boolean params may only default to false" );
break;
case 'integer':
$this->assertIsInt( $default,
"$param: Default $default is not an integer" );
break;
case 'limit':
if ( $default === 'max' ) {
break;
}
$this->assertIsInt( $default,
"$param: Default $default is neither an integer nor \"max\"" );
break;
case 'namespace':
$validValues = MediaWikiServices::getInstance()->getNamespaceInfo()->
getValidNamespaces();
if (
isset( $config[ApiBase::PARAM_EXTRA_NAMESPACES] ) &&
is_array( $config[ApiBase::PARAM_EXTRA_NAMESPACES] )
) {
$validValues = array_merge(
$validValues,
$config[ApiBase::PARAM_EXTRA_NAMESPACES]
);
}
$this->assertContains( $default, $validValues,
"$param: Default $default is not a valid namespace" );
break;
case 'NULL':
case 'password':
case 'string':
case 'submodule':
case 'tags':
case 'text':
$this->assertIsString( $default,
"$param: Default $default is not a string" );
break;
case 'timestamp':
if ( $default === 'now' ) {
return;
}
$this->assertNotFalse( wfTimestamp( TS_MW, $default ),
"$param: Default $default is not a valid timestamp" );
break;
case 'user':
// @todo Should we make user validation a public static method
// in ApiBase() or something so we don't have to resort to
// this? Or in User for that matter.
$wrapper = TestingAccessWrapper::newFromObject( new ApiMain() );
try {
$wrapper->validateUser( $default, '' );
} catch ( ApiUsageException $e ) {
$this->fail( "$param: Default $default is not a valid username/IP address" );
}
break;
default:
if ( is_array( $type ) ) {
$this->assertContains( $default, $type,
"$param: Default $default is not any of " .
implode( ', ', $type ) );
} else {
$this->fail( "Unrecognized type $type" );
}
}
}
/**
* @return array List of API module paths to test
*/
public static function provideParameterConsistency() {
public static function provideParameters() : Iterator {
$main = self::getMain();
$paths = self::getSubModulePaths( $main->getModuleManager() );
array_unshift( $paths, $main->getModulePath() );
$argsets = [
'plain' => [],
'for help' => [ ApiBase::GET_VALUES_FOR_HELP ],
];
$ret = [];
foreach ( $paths as $path ) {
$ret[] = [ $path ];
$module = $main->getModuleFromPath( $path );
foreach ( $argsets as $argset => $args ) {
$params = $module->getFinalParams( ...$args );
foreach ( $params as $param => $dummy ) {
yield "Module $path, $argset, parameter $param" => [ $path, $params, $param ];
}
}
}
return $ret;
}
/**