API: Use ParamValidator library

This brings significant modularization to the Action API's parameter
validation, and allows the Action API and MW REST API to share
validation code.

Note there are several changes in this patch that may affect other code;
see the entries in RELEASE-NOTES-1.35 for details.

Bug: T142080
Bug: T232672
Bug: T21195
Bug: T34675
Bug: T154774
Change-Id: I1462edc1701278760fa695308007006868b249fc
Depends-On: I10011be060fe6d27c7527312ad41218786b3f40d
This commit is contained in:
Brad Jorsch 2019-08-21 15:53:53 -04:00
parent 054dd94e97
commit c2b1525908
54 changed files with 3058 additions and 1664 deletions

View file

@ -142,9 +142,39 @@ For notes on 1.34.x and older releases, see HISTORY.
It was previously used by $wgEnableOpenSearchSuggest to partially
disable the API if set to false. Specifically, it would deny internal
frontend requests carrying this parameter, whilst accepting other requests.
* Integer-type parameters are now validated for syntax rather than being
interpreted in surprising ways. For example, the following will now return a
badinteger error:
* "1.9" (formerly interpreted as "1")
* " 1" (formerly interpreted as "1")
* "1e1" (formerly interpreted as "1" or "10", depending on the PHP version)
* "1foobar" (formerly interpreted as "1")
* "foobar" (formerly intepreted as "0")
* Error codes for many parameter validation failures are changing.
* action=paraminfo no longer returns "enforcerange" for numeric-typed
parameters. Ranges should be assumed to be enforced.
* Many user-type parameters now accept a user ID, formatted like "#12345".
* …
=== Action API internal changes in 1.35 ===
* The Action API now uses the Wikimedia\ParamValidator library for parameter
validation, which brings some new features and changes. For the most part
existing module code should work as it did before, but see subsequent notes
for changes.
* The values for all ApiBase PARAM_* constants have changed. Code should have
been using the constants rather than hard-coding the values.
* Several ApiBase PARAM_* constants have been deprecated, see the in-class
documentation for details. Use the equivalent ParamValidator constants
instead.
* The value returned for 'upload'-type parameters has changed from
WebRequestUpload to Psr\Http\Message\UploadedFileInterface.
* Validation of 'user'-type parameters is more flexible. PARAM constants exist
to specify the type of "user" allowed and to request UserIdentity objects
rather than name strings. The default is to accept all types (name, IP,
range, and interwiki) that were formerly accepted.
* Maximum limits are no longer ignored in "internal mode".
* The $paramName to ApiBase::handleParamNormalization() should now include the
prefix.
* …
=== Languages updated in 1.35 ===
@ -332,6 +362,20 @@ because of Phabricator reports.
* As part of dropping security support for IE 6 and IE 7,
WebRequest::checkUrlExtension() has been deprecated, and now always returns
true.
* The following ApiBase::PARAM_* constants have been deprecated in favor of
equivalent ParamValidator constants: PARAM_DFLT, PARAM_ISMULTI, PARAM_TYPE,
PARAM_MAX, PARAM_MAX2, PARAM_MIN, PARAM_ALLOW_DUPLICATES, PARAM_DEPRECATED,
PARAM_REQUIRED, PARAM_SUBMODULE_MAP, PARAM_SUBMODULE_PARAM_PREFIX, PARAM_ALL,
PARAM_EXTRA_NAMESPACES, PARAM_SENSITIVE, PARAM_DEPRECATED_VALUES,
PARAM_ISMULTI_LIMIT1, PARAM_ISMULTI_LIMIT2, PARAM_MAX_BYTES, PARAM_MAX_CHARS.
* ApiBase::explodeMultiValue() is deprecated. Use
ParamValidator::explodeMultiValue() instead.
* ApiBase::parseMultiValue() is deprecated. No replacement is provided;
generally this sort of thing should be handled by fully validating the
parameter.
* ApiBase::validateLimit() and ApiBase::validateTimestamp() are deprecated.
Use ApiParamValidator::validateValue() with an appropriate settings array
instead.
* $wgMemc is deprecated, use
MediaWikiServices::getInstance()->getLocalServerObjectCache() instead.
* ImagePage::getImageLimitsFromOptions() is deprecated. Use static function

View file

@ -857,6 +857,9 @@ $wgAutoloadLocalClasses = [
'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php',
'MediaWikiTitleCodec' => __DIR__ . '/includes/title/MediaWikiTitleCodec.php',
'MediaWikiVersionFetcher' => __DIR__ . '/includes/MediaWikiVersionFetcher.php',
'MediaWiki\\Api\\Validator\\ApiParamValidator' => __DIR__ . '/includes/api/Validator/ApiParamValidator.php',
'MediaWiki\\Api\\Validator\\ApiParamValidatorCallbacks' => __DIR__ . '/includes/api/Validator/ApiParamValidatorCallbacks.php',
'MediaWiki\\Api\\Validator\\SubmoduleDef' => __DIR__ . '/includes/api/Validator/SubmoduleDef.php',
'MediaWiki\\BadFileLookup' => __DIR__ . '/includes/BadFileLookup.php',
'MediaWiki\\Cache\\LinkBatchFactory' => __DIR__ . '/includes/cache/LinkBatchFactory.php',
'MediaWiki\\ChangeTags\\Taggable' => __DIR__ . '/includes/changetags/Taggable.php',

View file

@ -135,6 +135,7 @@ class AutoLoader {
'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/',
'MediaWiki\\Linker\\' => __DIR__ . '/linker/',
'MediaWiki\\Message\\' => __DIR__ . '/Message',
'MediaWiki\\ParamValidator\\' => __DIR__ . '/ParamValidator/',
'MediaWiki\\Permissions\\' => __DIR__ . '/Permissions/',
'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/',
'MediaWiki\\Rest\\' => __DIR__ . '/Rest/',

View file

@ -0,0 +1,78 @@
<?php
namespace MediaWiki\ParamValidator\TypeDef;
use ApiResult;
use NamespaceInfo;
use Wikimedia\ParamValidator\Callbacks;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef\EnumDef;
/**
* Type definition for namespace types
*
* A namespace type is an enum type that accepts MediaWiki namespace IDs.
*
* @since 1.35
*/
class NamespaceDef extends EnumDef {
/**
* (int[]) Additional namespace IDs to recognize.
*
* Generally this will be used to include NS_SPECIAL and/or NS_MEDIA.
*/
public const PARAM_EXTRA_NAMESPACES = 'param-extra-namespaces';
/** @var NamespaceInfo */
private $nsInfo;
public function __construct( Callbacks $callbacks, NamespaceInfo $nsInfo ) {
parent::__construct( $callbacks );
$this->nsInfo = $nsInfo;
}
public function validate( $name, $value, array $settings, array $options ) {
if ( !is_int( $value ) && preg_match( '/^[+-]?\d+$/D', $value ) ) {
// Convert to int since that's what getEnumValues() returns.
$value = (int)$value;
}
return parent::validate( $name, $value, $settings, $options );
}
public function getEnumValues( $name, array $settings, array $options ) {
$namespaces = $this->nsInfo->getValidNamespaces();
$extra = $settings[self::PARAM_EXTRA_NAMESPACES] ?? [];
if ( is_array( $extra ) && $extra !== [] ) {
$namespaces = array_merge( $namespaces, $extra );
}
sort( $namespaces );
return $namespaces;
}
public function normalizeSettings( array $settings ) {
// Force PARAM_ALL
if ( !empty( $settings[ParamValidator::PARAM_ISMULTI] ) ) {
$settings[ParamValidator::PARAM_ALL] = true;
}
return parent::normalizeSettings( $settings );
}
public function getParamInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );
$info['type'] = 'namespace';
$extra = $settings[self::PARAM_EXTRA_NAMESPACES] ?? [];
if ( is_array( $extra ) && $extra !== [] ) {
$info['extranamespaces'] = array_values( $extra );
if ( isset( $options['module'] ) ) {
// ApiResult metadata when used with the Action API.
ApiResult::setIndexedTagName( $info['extranamespaces'], 'ns' );
}
}
return $info;
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace MediaWiki\ParamValidator\TypeDef;
use ChangeTags;
use MediaWiki\Message\Converter as MessageConverter;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\Callbacks;
use Wikimedia\ParamValidator\TypeDef\EnumDef;
use Wikimedia\ParamValidator\ValidationException;
/**
* Type definition for tags type
*
* A tags type is an enum type for selecting MediaWiki change tags.
*
* Failure codes:
* - 'badtags': The value was not a valid set of tags. Data:
* - 'disallowedtags': The tags that were disallowed.
*
* @since 1.35
*/
class TagsDef extends EnumDef {
/** @var MessageConverter */
private $messageConverter;
public function __construct( Callbacks $callbacks ) {
parent::__construct( $callbacks );
$this->messageConverter = new MessageConverter();
}
public function validate( $name, $value, array $settings, array $options ) {
// Validate the full list of tags at once, because the caller will
// *probably* stop at the first exception thrown.
if ( isset( $options['values-list'] ) ) {
$ret = $value;
$tagsStatus = ChangeTags::canAddTagsAccompanyingChange( $options['values-list'] );
} else {
// The 'tags' type always returns an array.
$ret = [ $value ];
$tagsStatus = ChangeTags::canAddTagsAccompanyingChange( $ret );
}
if ( !$tagsStatus->isGood() ) {
$msg = $this->messageConverter->convertMessage( $tagsStatus->getMessage() );
$data = [];
if ( $tagsStatus->value ) {
// Specific tags are not allowed.
$data['disallowedtags'] = $tagsStatus->value;
// @codeCoverageIgnoreStart
} else {
// All are disallowed, I guess
$data['disallowedtags'] = $settings['values-list'] ?? $ret;
}
// @codeCoverageIgnoreEnd
// Only throw if $value is among the disallowed tags
if ( in_array( $value, $data['disallowedtags'], true ) ) {
throw new ValidationException(
DataMessageValue::new( $msg->getKey(), $msg->getParams(), 'badtags', $data ),
$name, $value, $settings
);
}
}
return $ret;
}
public function getEnumValues( $name, array $settings, array $options ) {
return ChangeTags::listExplicitlyDefinedTags();
}
}

View file

@ -0,0 +1,169 @@
<?php
namespace MediaWiki\ParamValidator\TypeDef;
use ExternalUserNames;
// phpcs:ignore MediaWiki.Classes.UnusedUseStatement.UnusedUse
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserIdentityValue;
use Title;
use User;
use Wikimedia\IPUtils;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef;
/**
* Type definition for user types
*
* Failure codes:
* - 'baduser': The value was not a valid MediaWiki user. No data.
*
* @since 1.35
*/
class UserDef extends TypeDef {
/**
* (string[]) Allowed types of user.
*
* One or more of the following values:
* - 'name': User names are allowed.
* - 'ip': IP ("anon") usernames are allowed.
* - 'cidr': IP ranges are allowed.
* - 'interwiki': Interwiki usernames are allowed.
* - 'id': Allow specifying user IDs, formatted like "#123".
*
* Default is `[ 'name', 'ip', 'cidr', 'interwiki' ]`.
*
* Avoid combining 'id' with PARAM_ISMULTI, as it may result in excessive
* DB lookups. If you do combine them, consider setting low values for
* PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2 to mitigate it.
*/
public const PARAM_ALLOWED_USER_TYPES = 'param-allowed-user-types';
/**
* (bool) Whether to return a UserIdentity object.
*
* If false, the validated user name is returned as a string. Default is false.
*
* Avoid setting true with PARAM_ISMULTI, as it may result in excessive DB
* lookups. If you do combine them, consider setting low values for
* PARAM_ISMULTI_LIMIT1 and PARAM_ISMULTI_LIMIT2 to mitigate it.
*/
public const PARAM_RETURN_OBJECT = 'param-return-object';
public function validate( $name, $value, array $settings, array $options ) {
list( $type, $user ) = $this->processUser( $value );
if ( !$user || !in_array( $type, $settings[self::PARAM_ALLOWED_USER_TYPES], true ) ) {
$this->failure( 'baduser', $name, $value, $settings, $options );
}
return empty( $settings[self::PARAM_RETURN_OBJECT] ) ? $user->getName() : $user;
}
public function normalizeSettings( array $settings ) {
if ( isset( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
$settings[self::PARAM_ALLOWED_USER_TYPES] = array_values( array_intersect(
[ 'name', 'ip', 'cidr', 'interwiki', 'id' ],
$settings[self::PARAM_ALLOWED_USER_TYPES]
) );
}
if ( empty( $settings[self::PARAM_ALLOWED_USER_TYPES] ) ) {
$settings[self::PARAM_ALLOWED_USER_TYPES] = [ 'name', 'ip', 'cidr', 'interwiki' ];
}
return parent::normalizeSettings( $settings );
}
/**
* Process $value to a UserIdentity, if possible
* @param string $value
* @return array [ string $type, UserIdentity|null $user ]
* @phan-return array{0:string,1:UserIdentity|null}
*/
private function processUser( string $value ) : array {
// A user ID?
if ( preg_match( '/^#(\d+)$/D', $value, $m ) ) {
return [ 'id', User::newFromId( $m[1] ) ];
}
// An interwiki username?
if ( ExternalUserNames::isExternal( $value ) ) {
$name = User::getCanonicalName( $value, false );
return [
'interwiki',
is_string( $name ) ? new UserIdentityValue( 0, $value, 0 ) : null
];
}
// A valid user name?
$user = User::newFromName( $value, 'valid' );
if ( $user ) {
return [ 'name', $user ];
}
// (T232672) Reproduce the normalization applied in User::getCanonicalName() when
// performing the checks below.
if ( strpos( $value, '#' ) !== false ) {
return [ '', null ];
}
$t = Title::newFromText( $value ); // In case of explicit "User:" prefix, sigh.
if ( !$t || $t->getNamespace() !== NS_USER || $t->isExternal() ) { // likely
$t = Title::newFromText( "User:$value" );
}
if ( !$t || $t->getNamespace() !== NS_USER || $t->isExternal() ) {
// If it wasn't a valid User-namespace title, fail.
return [ '', null ];
}
$value = $t->getText();
// An IP?
$b = IPUtils::RE_IP_BYTE;
if ( IPUtils::isValid( $value ) ||
// See comment for User::isIP. We don't just call that function
// here because it also returns true for things like
// 300.300.300.300 that are neither valid usernames nor valid IP
// addresses.
preg_match( "/^$b\.$b\.$b\.xxx$/D", $value )
) {
return [ 'ip', new UserIdentityValue( 0, IPUtils::sanitizeIP( $value ), 0 ) ];
}
// A range?
if ( IPUtils::isValidRange( $value ) ) {
return [ 'cidr', new UserIdentityValue( 0, IPUtils::sanitizeIP( $value ), 0 ) ];
}
// Fail.
return [ '', null ];
}
public function getParamInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );
$info['subtypes'] = $settings[self::PARAM_ALLOWED_USER_TYPES];
return $info;
}
public function getHelpInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );
$isMulti = !empty( $settings[ParamValidator::PARAM_ISMULTI] );
$subtypes = [];
foreach ( $settings[self::PARAM_ALLOWED_USER_TYPES] as $st ) {
// Messages: paramvalidator-help-type-user-subtype-name,
// paramvalidator-help-type-user-subtype-ip, paramvalidator-help-type-user-subtype-cidr,
// paramvalidator-help-type-user-subtype-interwiki, paramvalidator-help-type-user-subtype-id
$subtypes[] = MessageValue::new( "paramvalidator-help-type-user-subtype-$st" );
}
$info[ParamValidator::PARAM_TYPE] = MessageValue::new( 'paramvalidator-help-type-user' )
->params( $isMulti ? 2 : 1 )
->textListParams( $subtypes )
->numParams( count( $subtypes ) );
return $info;
}
}

View file

@ -101,6 +101,20 @@ class WebRequestUpload {
return $this->fileInfo['tmp_name'];
}
/**
* Return the client specified content type
*
* @since 1.35
* @return string|null Type or null if non-existent
*/
public function getType() {
if ( !$this->exists() ) {
return null;
}
return $this->fileInfo['type'];
}
/**
* Return the upload error. See link for explanation
* https://www.php.net/manual/en/features.file-upload.errors.php

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\ParamValidator\TypeDef\UserDef;
/**
* API module that facilitates the blocking of users. Requires API write mode
@ -186,9 +187,11 @@ class ApiBlock extends ApiBase {
$params = [
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'cidr', 'id' ],
],
'userid' => [
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_DEPRECATED => true,
],
'expiry' => 'never',
'reason' => '',

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Revision\RevisionAccessException;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
@ -69,17 +70,13 @@ class ApiFeedContributions extends ApiBase {
$msg = wfMessage( 'Contributions' )->inContentLanguage()->text();
$feedTitle = $config->get( 'Sitename' ) . ' - ' . $msg .
' [' . $config->get( 'LanguageCode' ) . ']';
$feedUrl = SpecialPage::getTitleFor( 'Contributions', $params['user'] )->getFullURL();
try {
$target = $this->titleParser
->parseTitle( $params['user'], NS_USER )
->getText();
} catch ( MalformedTitleException $e ) {
$this->dieWithError(
[ 'apierror-baduser', 'user', wfEscapeWikiText( $params['user'] ) ],
'baduser_' . $this->encodeParamName( 'user' )
);
$target = $params['user'];
if ( ExternalUserNames::isExternal( $target ) ) {
// Interwiki names make invalid titles, so put the target in the query instead.
$feedUrl = SpecialPage::getTitleFor( 'Contributions' )->getFullURL( [ 'target' => $target ] );
} else {
$feedUrl = SpecialPage::getTitleFor( 'Contributions', $target )->getFullURL();
}
$feed = new $feedClasses[$params['feedformat']] (
@ -220,6 +217,7 @@ class ApiFeedContributions extends ApiBase {
],
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'cidr', 'id', 'interwiki' ],
ApiBase::PARAM_REQUIRED => true,
],
'namespace' => [

View file

@ -22,6 +22,7 @@
use HtmlFormatter\HtmlFormatter;
use MediaWiki\MediaWikiServices;
use Wikimedia\ParamValidator\ParamValidator;
/**
* Class to output help for an API module
@ -253,6 +254,7 @@ class ApiHelp extends ApiBase {
}
foreach ( $modules as $module ) {
$paramValidator = $module->getMain()->getParamValidator();
$tocnumber[$level]++;
$path = $module->getModulePath();
$module->setContext( $context );
@ -448,8 +450,10 @@ class ApiHelp extends ApiBase {
$descriptions = $module->getFinalParamDescription();
foreach ( $params as $name => $settings ) {
if ( !is_array( $settings ) ) {
$settings = [ ApiBase::PARAM_DFLT => $settings ];
$settings = $paramValidator->normalizeSettings( $settings );
if ( $settings[ApiBase::PARAM_TYPE] === 'submodule' ) {
$groups[] = $name;
}
$help['parameters'] .= Html::rawElement( 'dt', null,
@ -464,13 +468,41 @@ class ApiHelp extends ApiBase {
$description[] = $msg->parseAsBlock();
}
}
if ( !array_filter( $description ) ) {
$description = [ self::wrap(
$context->msg( 'api-help-param-no-description' ),
'apihelp-empty'
) ];
}
// Add "deprecated" flag
if ( !empty( $settings[ApiBase::PARAM_DEPRECATED] ) ) {
$help['parameters'] .= Html::openElement( 'dd',
[ 'class' => 'info' ] );
$help['parameters'] .= self::wrap(
$context->msg( 'api-help-param-deprecated' ),
'apihelp-deprecated', 'strong'
);
$help['parameters'] .= Html::closeElement( 'dd' );
}
if ( $description ) {
$description = implode( '', $description );
$description = preg_replace( '!\s*</([oud]l)>\s*<\1>\s*!', "\n", $description );
$help['parameters'] .= Html::rawElement( 'dd',
[ 'class' => 'description' ], $description );
}
// Add usage info
$info = [];
$paramHelp = $paramValidator->getHelpInfo( $module, $name, $settings, [] );
// Required?
if ( !empty( $settings[ApiBase::PARAM_REQUIRED] ) ) {
$info[] = $context->msg( 'api-help-param-required' )->parse();
unset( $paramHelp[ParamValidator::PARAM_DEPRECATED] );
if ( isset( $paramHelp[ParamValidator::PARAM_REQUIRED] ) ) {
$paramHelp[ParamValidator::PARAM_REQUIRED]->setContext( $context );
$info[] = $paramHelp[ParamValidator::PARAM_REQUIRED];
unset( $paramHelp[ParamValidator::PARAM_REQUIRED] );
}
// Custom info?
@ -500,288 +532,9 @@ class ApiHelp extends ApiBase {
}
// Type documentation
if ( !isset( $settings[ApiBase::PARAM_TYPE] ) ) {
$dflt = $settings[ApiBase::PARAM_DFLT] ?? null;
if ( is_bool( $dflt ) ) {
$settings[ApiBase::PARAM_TYPE] = 'boolean';
} elseif ( is_string( $dflt ) || $dflt === null ) {
$settings[ApiBase::PARAM_TYPE] = 'string';
} elseif ( is_int( $dflt ) ) {
$settings[ApiBase::PARAM_TYPE] = 'integer';
}
}
if ( isset( $settings[ApiBase::PARAM_TYPE] ) ) {
$type = $settings[ApiBase::PARAM_TYPE];
$multi = !empty( $settings[ApiBase::PARAM_ISMULTI] );
$hintPipeSeparated = true;
$count = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT2] )
? $settings[ApiBase::PARAM_ISMULTI_LIMIT2] + 1
: ApiBase::LIMIT_SML2 + 1;
if ( is_array( $type ) ) {
$count = count( $type );
$deprecatedValues = $settings[ApiBase::PARAM_DEPRECATED_VALUES] ?? [];
$links = $settings[ApiBase::PARAM_VALUE_LINKS] ?? [];
$values = array_map( function ( $v ) use ( $links, $deprecatedValues ) {
$attr = [];
if ( $v !== '' ) {
// We can't know whether this contains LTR or RTL text.
$attr['dir'] = 'auto';
}
if ( isset( $deprecatedValues[$v] ) ) {
$attr['class'] = 'apihelp-deprecated-value';
}
$ret = $attr ? Html::element( 'span', $attr, $v ) : $v;
if ( isset( $links[$v] ) ) {
$ret = "[[{$links[$v]}|$ret]]";
}
return $ret;
}, $type );
$i = array_search( '', $type, true );
if ( $i === false ) {
$values = $context->getLanguage()->commaList( $values );
} else {
unset( $values[$i] );
$values = $context->msg( 'api-help-param-list-can-be-empty' )
->numParams( count( $values ) )
->params( $context->getLanguage()->commaList( $values ) )
->parse();
}
$info[] = $context->msg( 'api-help-param-list' )
->params( $multi ? 2 : 1 )
->params( $values )
->parse();
$hintPipeSeparated = false;
} else {
switch ( $type ) {
case 'submodule':
$groups[] = $name;
if ( isset( $settings[ApiBase::PARAM_SUBMODULE_MAP] ) ) {
$map = $settings[ApiBase::PARAM_SUBMODULE_MAP];
$defaultAttrs = [];
} else {
$prefix = $module->isMain() ? '' : ( $module->getModulePath() . '+' );
$map = [];
foreach ( $module->getModuleManager()->getNames( $name ) as $submoduleName ) {
$map[$submoduleName] = $prefix . $submoduleName;
}
$defaultAttrs = [ 'dir' => 'ltr', 'lang' => 'en' ];
}
$submodules = [];
$submoduleFlags = []; // for sorting: higher flags are sorted later
$submoduleNames = []; // for sorting: lexicographical, ascending
foreach ( $map as $v => $m ) {
$attrs = $defaultAttrs;
$flags = 0;
try {
$submod = $module->getModuleFromPath( $m );
if ( $submod && $submod->isDeprecated() ) {
$attrs['class'][] = 'apihelp-deprecated-value';
$flags |= 1;
}
if ( $submod && $submod->isInternal() ) {
$attrs['class'][] = 'apihelp-internal-value';
$flags |= 2;
}
} catch ( ApiUsageException $ex ) {
// Ignore
}
$v = Html::element( 'span', $attrs, $v );
$submodules[] = "[[Special:ApiHelp/{$m}|{$v}]]";
$submoduleFlags[] = $flags;
$submoduleNames[] = $v;
}
// sort $submodules by $submoduleFlags and $submoduleNames
array_multisort( $submoduleFlags, $submoduleNames, $submodules );
$count = count( $submodules );
$info[] = $context->msg( 'api-help-param-list' )
->params( $multi ? 2 : 1 )
->params( $context->getLanguage()->commaList( $submodules ) )
->parse();
$hintPipeSeparated = false;
// No type message necessary, we have a list of values.
$type = null;
break;
case 'namespace':
$namespaces = MediaWikiServices::getInstance()->
getNamespaceInfo()->getValidNamespaces();
if ( isset( $settings[ApiBase::PARAM_EXTRA_NAMESPACES] ) &&
is_array( $settings[ApiBase::PARAM_EXTRA_NAMESPACES] )
) {
$namespaces = array_merge( $namespaces, $settings[ApiBase::PARAM_EXTRA_NAMESPACES] );
}
sort( $namespaces );
$count = count( $namespaces );
$info[] = $context->msg( 'api-help-param-list' )
->params( $multi ? 2 : 1 )
->params( $context->getLanguage()->commaList( $namespaces ) )
->parse();
$hintPipeSeparated = false;
// No type message necessary, we have a list of values.
$type = null;
break;
case 'tags':
$tags = ChangeTags::listExplicitlyDefinedTags();
$count = count( $tags );
$info[] = $context->msg( 'api-help-param-list' )
->params( $multi ? 2 : 1 )
->params( $context->getLanguage()->commaList( $tags ) )
->parse();
$hintPipeSeparated = false;
$type = null;
break;
case 'limit':
if ( isset( $settings[ApiBase::PARAM_MAX2] ) ) {
$info[] = $context->msg( 'api-help-param-limit2' )
->numParams( $settings[ApiBase::PARAM_MAX] )
->numParams( $settings[ApiBase::PARAM_MAX2] )
->parse();
} else {
$info[] = $context->msg( 'api-help-param-limit' )
->numParams( $settings[ApiBase::PARAM_MAX] )
->parse();
}
break;
case 'integer':
// Possible messages:
// api-help-param-integer-min,
// api-help-param-integer-max,
// api-help-param-integer-minmax
$suffix = '';
$min = $max = 0;
if ( isset( $settings[ApiBase::PARAM_MIN] ) ) {
$suffix .= 'min';
$min = $settings[ApiBase::PARAM_MIN];
}
if ( isset( $settings[ApiBase::PARAM_MAX] ) ) {
$suffix .= 'max';
$max = $settings[ApiBase::PARAM_MAX];
}
if ( $suffix !== '' ) {
$info[] =
$context->msg( "api-help-param-integer-$suffix" )
->params( $multi ? 2 : 1 )
->numParams( $min, $max )
->parse();
}
break;
case 'upload':
$info[] = $context->msg( 'api-help-param-upload' )
->parse();
// No type message necessary, api-help-param-upload should handle it.
$type = null;
break;
case 'string':
case 'text':
// Displaying a type message here would be useless.
$type = null;
break;
}
}
// Add type. Messages for grep: api-help-param-type-limit
// api-help-param-type-integer api-help-param-type-boolean
// api-help-param-type-timestamp api-help-param-type-user
// api-help-param-type-password
if ( is_string( $type ) ) {
$msg = $context->msg( "api-help-param-type-$type" );
if ( !$msg->isDisabled() ) {
$info[] = $msg->params( $multi ? 2 : 1 )->parse();
}
}
if ( $multi ) {
$extra = [];
$lowcount = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT1] )
? $settings[ApiBase::PARAM_ISMULTI_LIMIT1]
: ApiBase::LIMIT_SML1;
$highcount = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT2] )
? $settings[ApiBase::PARAM_ISMULTI_LIMIT2]
: ApiBase::LIMIT_SML2;
if ( $hintPipeSeparated ) {
$extra[] = $context->msg( 'api-help-param-multi-separate' )->parse();
}
if ( $count > $lowcount ) {
if ( $lowcount === $highcount ) {
$msg = $context->msg( 'api-help-param-multi-max-simple' )
->numParams( $lowcount );
} else {
$msg = $context->msg( 'api-help-param-multi-max' )
->numParams( $lowcount, $highcount );
}
$extra[] = $msg->parse();
}
if ( $extra ) {
$info[] = implode( ' ', $extra );
}
$allowAll = $settings[ApiBase::PARAM_ALL] ?? false;
if ( $allowAll || $settings[ApiBase::PARAM_TYPE] === 'namespace' ) {
if ( $settings[ApiBase::PARAM_TYPE] === 'namespace' ) {
$allSpecifier = ApiBase::ALL_DEFAULT_STRING;
} else {
$allSpecifier = ( is_string( $allowAll ) ? $allowAll : ApiBase::ALL_DEFAULT_STRING );
}
$info[] = $context->msg( 'api-help-param-multi-all' )
->params( $allSpecifier )
->parse();
}
}
}
if ( isset( $settings[self::PARAM_MAX_BYTES] ) ) {
$info[] = $context->msg( 'api-help-param-maxbytes' )
->numParams( $settings[self::PARAM_MAX_BYTES] );
}
if ( isset( $settings[self::PARAM_MAX_CHARS] ) ) {
$info[] = $context->msg( 'api-help-param-maxchars' )
->numParams( $settings[self::PARAM_MAX_CHARS] );
}
// Add default
$default = $settings[ApiBase::PARAM_DFLT] ?? null;
if ( $default === '' ) {
$info[] = $context->msg( 'api-help-param-default-empty' )
->parse();
} elseif ( $default !== null && $default !== false ) {
// We can't know whether this contains LTR or RTL text.
$info[] = $context->msg( 'api-help-param-default' )
->params( Html::element( 'span', [ 'dir' => 'auto' ], $default ) )
->parse();
}
if ( !array_filter( $description ) ) {
$description = [ self::wrap(
$context->msg( 'api-help-param-no-description' ),
'apihelp-empty'
) ];
}
// Add "deprecated" flag
if ( !empty( $settings[ApiBase::PARAM_DEPRECATED] ) ) {
$help['parameters'] .= Html::openElement( 'dd',
[ 'class' => 'info' ] );
$help['parameters'] .= self::wrap(
$context->msg( 'api-help-param-deprecated' ),
'apihelp-deprecated', 'strong'
);
$help['parameters'] .= Html::closeElement( 'dd' );
}
if ( $description ) {
$description = implode( '', $description );
$description = preg_replace( '!\s*</([oud]l)>\s*<\1>\s*!', "\n", $description );
$help['parameters'] .= Html::rawElement( 'dd',
[ 'class' => 'description' ], $description );
foreach ( $paramHelp as $m ) {
$m->setContext( $context );
$info[] = $m;
}
foreach ( $info as $i ) {

View file

@ -21,8 +21,10 @@
* @defgroup API API
*/
use MediaWiki\Api\Validator\ApiParamValidator;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Session\SessionManager;
use Wikimedia\Timestamp\TimestampException;
@ -144,7 +146,7 @@ class ApiMain extends ApiBase {
*/
private $mPrinter;
private $mModuleMgr, $mResult, $mErrorFormatter = null;
private $mModuleMgr, $mResult, $mErrorFormatter = null, $mParamValidator;
/** @var ApiContinuationManager|null */
private $mContinuationManager;
private $mAction;
@ -237,6 +239,10 @@ class ApiMain extends ApiBase {
}
}
$this->mParamValidator = new ApiParamValidator(
$this, MediaWikiServices::getInstance()->getObjectFactory()
);
$this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
// Setup uselang. This doesn't use $this->getParameter()
@ -382,6 +388,14 @@ class ApiMain extends ApiBase {
$this->mContinuationManager = $manager;
}
/**
* Get the parameter validator
* @return ApiParamValidator
*/
public function getParamValidator() : ApiParamValidator {
return $this->mParamValidator;
}
/**
* Get the API module object. Only works after executeAction()
*
@ -1788,7 +1802,8 @@ class ApiMain extends ApiBase {
* @return bool
*/
public function getCheck( $name ) {
return $this->getVal( $name, null ) !== null;
$this->mParamsUsed[$name] = true;
return $this->getRequest()->getCheck( $name );
}
/**
@ -1888,6 +1903,7 @@ class ApiMain extends ApiBase {
],
'assertuser' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
],
'requestid' => null,
'servedby' => false,
@ -1990,7 +2006,25 @@ class ApiMain extends ApiBase {
$headline = '<div id="main/datatypes"></div>' . $headline;
}
$help['datatypes'] .= $headline;
$help['datatypes'] .= $this->msg( 'api-help-datatypes' )->parseAsBlock();
$help['datatypes'] .= $this->msg( 'api-help-datatypes-top' )->parseAsBlock();
$help['datatypes'] .= '<dl>';
foreach ( $this->getParamValidator()->knownTypes() as $type ) {
$m = $this->msg( "api-help-datatype-$type" );
if ( !$m->isDisabled() ) {
$id = "main/datatype/$type";
$help['datatypes'] .= '<dt id="' . htmlspecialchars( $id ) . '">';
$encId = Sanitizer::escapeIdForAttribute( $id, Sanitizer::ID_PRIMARY );
if ( $encId !== $id ) {
$help['datatypes'] .= '<span id="' . htmlspecialchars( $encId ) . '"></span>';
}
$encId2 = Sanitizer::escapeIdForAttribute( $id, Sanitizer::ID_FALLBACK );
if ( $encId2 !== $id && $encId2 !== $encId ) {
$help['datatypes'] .= '<span id="' . htmlspecialchars( $encId2 ) . '"></span>';
}
$help['datatypes'] .= htmlspecialchars( $type ) . '</dt><dd>' . $m->parseAsBlock() . "</dd>";
}
}
$help['datatypes'] .= '</dl>';
if ( !isset( $tocData['main/datatypes'] ) ) {
$tocnumber[$level]++;
$tocData['main/datatypes'] = [

View file

@ -19,7 +19,9 @@
*
* @file
*/
use MediaWiki\MediaWikiServices;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\IResultWrapper;
@ -1471,15 +1473,15 @@ class ApiPageSet extends ApiBase {
return $result;
}
protected function handleParamNormalization( $paramName, $value, $rawValue ) {
public function handleParamNormalization( $paramName, $value, $rawValue ) {
parent::handleParamNormalization( $paramName, $value, $rawValue );
if ( $paramName === 'titles' ) {
// For the 'titles' parameter, we want to split it like ApiBase would
// and add any changed titles to $this->mNormalizedTitles
$value = $this->explodeMultiValue( $value, self::LIMIT_SML2 + 1 );
$value = ParamValidator::explodeMultiValue( $value, self::LIMIT_SML2 + 1 );
$l = count( $value );
$rawValue = $this->explodeMultiValue( $rawValue, $l );
$rawValue = ParamValidator::explodeMultiValue( $rawValue, $l );
for ( $i = 0; $i < $l; $i++ ) {
if ( $value[$i] !== $rawValue[$i] ) {
$this->mNormalizedTitles[$rawValue[$i]] = $value[$i];

View file

@ -238,6 +238,7 @@ class ApiParamInfo extends ApiBase {
private function getModuleInfo( $module ) {
$ret = [];
$path = $module->getModulePath();
$paramValidator = $module->getMain()->getParamValidator();
$ret['name'] = $module->getModuleName();
$ret['classname'] = get_class( $module );
@ -310,9 +311,7 @@ class ApiParamInfo extends ApiBase {
$paramDesc = $module->getFinalParamDescription();
$index = 0;
foreach ( $params as $name => $settings ) {
if ( !is_array( $settings ) ) {
$settings = [ ApiBase::PARAM_DFLT => $settings ];
}
$settings = $paramValidator->normalizeSettings( $settings );
$item = [
'index' => ++$index,
@ -328,175 +327,20 @@ class ApiParamInfo extends ApiBase {
$this->formatHelpMessages( $item, 'description', $paramDesc[$name], true );
}
$item['required'] = !empty( $settings[ApiBase::PARAM_REQUIRED] );
if ( !empty( $settings[ApiBase::PARAM_DEPRECATED] ) ) {
$item['deprecated'] = true;
foreach ( $paramValidator->getParamInfo( $module, $name, $settings, [] ) as $k => $v ) {
$item[$k] = $v;
}
if ( $name === 'token' && $module->needsToken() ) {
$item['tokentype'] = $module->needsToken();
}
if ( !isset( $settings[ApiBase::PARAM_TYPE] ) ) {
$dflt = $settings[ApiBase::PARAM_DFLT] ?? null;
if ( is_bool( $dflt ) ) {
$settings[ApiBase::PARAM_TYPE] = 'boolean';
} elseif ( is_string( $dflt ) || $dflt === null ) {
$settings[ApiBase::PARAM_TYPE] = 'string';
} elseif ( is_int( $dflt ) ) {
$settings[ApiBase::PARAM_TYPE] = 'integer';
}
}
if ( isset( $settings[ApiBase::PARAM_DFLT] ) ) {
switch ( $settings[ApiBase::PARAM_TYPE] ) {
case 'boolean':
$item['default'] = (bool)$settings[ApiBase::PARAM_DFLT];
break;
case 'string':
case 'text':
case 'password':
$item['default'] = strval( $settings[ApiBase::PARAM_DFLT] );
break;
case 'integer':
case 'limit':
$item['default'] = (int)$settings[ApiBase::PARAM_DFLT];
break;
case 'timestamp':
$item['default'] = wfTimestamp( TS_ISO_8601, $settings[ApiBase::PARAM_DFLT] );
break;
default:
$item['default'] = $settings[ApiBase::PARAM_DFLT];
break;
}
}
$item['multi'] = !empty( $settings[ApiBase::PARAM_ISMULTI] );
if ( $item['multi'] ) {
$item['lowlimit'] = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT1] )
? $settings[ApiBase::PARAM_ISMULTI_LIMIT1]
: ApiBase::LIMIT_SML1;
$item['highlimit'] = !empty( $settings[ApiBase::PARAM_ISMULTI_LIMIT2] )
? $settings[ApiBase::PARAM_ISMULTI_LIMIT2]
: ApiBase::LIMIT_SML2;
$item['limit'] = $this->getMain()->canApiHighLimits()
? $item['highlimit']
: $item['lowlimit'];
}
if ( !empty( $settings[ApiBase::PARAM_ALLOW_DUPLICATES] ) ) {
$item['allowsduplicates'] = true;
}
if ( isset( $settings[ApiBase::PARAM_TYPE] ) ) {
if ( $settings[ApiBase::PARAM_TYPE] === 'submodule' ) {
if ( isset( $settings[ApiBase::PARAM_SUBMODULE_MAP] ) ) {
$item['type'] = array_keys( $settings[ApiBase::PARAM_SUBMODULE_MAP] );
$item['submodules'] = $settings[ApiBase::PARAM_SUBMODULE_MAP];
} else {
$item['type'] = $module->getModuleManager()->getNames( $name );
$prefix = $module->isMain()
? '' : ( $module->getModulePath() . '+' );
$item['submodules'] = [];
foreach ( $item['type'] as $v ) {
$item['submodules'][$v] = $prefix . $v;
}
}
if ( isset( $settings[ApiBase::PARAM_SUBMODULE_PARAM_PREFIX] ) ) {
$item['submoduleparamprefix'] = $settings[ApiBase::PARAM_SUBMODULE_PARAM_PREFIX];
}
$submoduleFlags = []; // for sorting: higher flags are sorted later
$submoduleNames = []; // for sorting: lexicographical, ascending
foreach ( $item['submodules'] as $v => $submodulePath ) {
try {
$submod = $this->getModuleFromPath( $submodulePath );
} catch ( ApiUsageException $ex ) {
$submoduleFlags[] = 0;
$submoduleNames[] = $v;
continue;
}
$flags = 0;
if ( $submod && $submod->isDeprecated() ) {
$item['deprecatedvalues'][] = $v;
$flags |= 1;
}
if ( $submod && $submod->isInternal() ) {
$item['internalvalues'][] = $v;
$flags |= 2;
}
$submoduleFlags[] = $flags;
$submoduleNames[] = $v;
}
// sort $item['submodules'] and $item['type'] by $submoduleFlags and $submoduleNames
array_multisort( $submoduleFlags, $submoduleNames, $item['submodules'], $item['type'] );
if ( isset( $item['deprecatedvalues'] ) ) {
sort( $item['deprecatedvalues'] );
}
if ( isset( $item['internalvalues'] ) ) {
sort( $item['internalvalues'] );
}
} elseif ( $settings[ApiBase::PARAM_TYPE] === 'tags' ) {
$item['type'] = ChangeTags::listExplicitlyDefinedTags();
} else {
$item['type'] = $settings[ApiBase::PARAM_TYPE];
}
if ( is_array( $item['type'] ) ) {
// To prevent sparse arrays from being serialized to JSON as objects
$item['type'] = array_values( $item['type'] );
ApiResult::setIndexedTagName( $item['type'], 't' );
}
// Add 'allspecifier' if applicable
if ( $item['type'] === 'namespace' ) {
$allowAll = true;
$allSpecifier = ApiBase::ALL_DEFAULT_STRING;
} else {
$allowAll = $settings[ApiBase::PARAM_ALL] ?? false;
$allSpecifier = ( is_string( $allowAll ) ? $allowAll : ApiBase::ALL_DEFAULT_STRING );
}
if ( $allowAll && $item['multi'] &&
( is_array( $item['type'] ) || $item['type'] === 'namespace' ) ) {
$item['allspecifier'] = $allSpecifier;
}
if ( $item['type'] === 'namespace' &&
isset( $settings[ApiBase::PARAM_EXTRA_NAMESPACES] ) &&
is_array( $settings[ApiBase::PARAM_EXTRA_NAMESPACES] )
) {
$item['extranamespaces'] = $settings[ApiBase::PARAM_EXTRA_NAMESPACES];
ApiResult::setArrayType( $item['extranamespaces'], 'array' );
ApiResult::setIndexedTagName( $item['extranamespaces'], 'ns' );
}
}
if ( isset( $settings[ApiBase::PARAM_MAX] ) ) {
$item['max'] = $settings[ApiBase::PARAM_MAX];
}
if ( isset( $settings[ApiBase::PARAM_MAX2] ) ) {
$item['highmax'] = $settings[ApiBase::PARAM_MAX2];
}
if ( isset( $settings[ApiBase::PARAM_MIN] ) ) {
$item['min'] = $settings[ApiBase::PARAM_MIN];
}
if ( !empty( $settings[ApiBase::PARAM_RANGE_ENFORCE] ) ) {
$item['enforcerange'] = true;
}
if ( isset( $settings[self::PARAM_MAX_BYTES] ) ) {
$item['maxbytes'] = $settings[self::PARAM_MAX_BYTES];
}
if ( isset( $settings[self::PARAM_MAX_CHARS] ) ) {
$item['maxchars'] = $settings[self::PARAM_MAX_CHARS];
}
if ( !empty( $settings[ApiBase::PARAM_DEPRECATED_VALUES] ) ) {
$deprecatedValues = array_keys( $settings[ApiBase::PARAM_DEPRECATED_VALUES] );
if ( is_array( $item['type'] ) ) {
$deprecatedValues = array_intersect( $deprecatedValues, $item['type'] );
}
if ( $deprecatedValues ) {
$item['deprecatedvalues'] = array_values( $deprecatedValues );
ApiResult::setIndexedTagName( $item['deprecatedvalues'], 'v' );
}
if ( $item['type'] === 'NULL' ) {
// Munge "NULL" to "string" for historical reasons
$item['type'] = 'string';
} elseif ( is_array( $item['type'] ) ) {
// Set indexed tag name, for historical reasons
ApiResult::setIndexedTagName( $item['type'], 't' );
}
if ( !empty( $settings[ApiBase::PARAM_HELP_MSG_INFO] ) ) {

View file

@ -24,6 +24,7 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Storage\NameTableAccessException;
@ -224,14 +225,14 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
if ( $params['user'] !== null ) {
// Don't query by user ID here, it might be able to use the ar_usertext_timestamp index.
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false );
->getWhere( $db, 'ar_user', $params['user'], false );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( $actorQuery['conds'] );
} elseif ( $params['excludeuser'] !== null ) {
// Here there's no chance of using ar_usertext_timestamp.
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) );
->getWhere( $db, 'ar_user', $params['excludeuser'] );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
@ -402,7 +403,9 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
public function getAllowedParams() {
$ret = parent::getAllowedParams() + [
'user' => [
ApiBase::PARAM_TYPE => 'user'
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'namespace' => [
ApiBase::PARAM_ISMULTI => true,
@ -435,6 +438,8 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
],
'excludeuser' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
ApiBase::PARAM_HELP_MSG_INFO => [ [ 'nonuseronly' ] ],
],
'tag' => null,

View file

@ -24,6 +24,7 @@
* @file
*/
use MediaWiki\ParamValidator\TypeDef\UserDef;
use Wikimedia\Rdbms\IDatabase;
/**
@ -192,7 +193,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
// Image filters
if ( $params['user'] !== null ) {
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'img_user', User::newFromName( $params['user'], false ) );
->getWhere( $db, 'img_user', $params['user'] );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( $actorQuery['conds'] );
@ -372,7 +373,9 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
'sha1' => null,
'sha1base36' => null,
'user' => [
ApiBase::PARAM_TYPE => 'user'
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'filterbots' => [
ApiBase::PARAM_DFLT => 'all',

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Revision\RevisionRecord;
/**
@ -140,11 +141,11 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
if ( $params['user'] !== null ) {
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'rev_user', User::newFromName( $params['user'], false ) );
->getWhere( $db, 'rev_user', $params['user'] );
$this->addWhere( $actorQuery['conds'] );
} elseif ( $params['excludeuser'] !== null ) {
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'rev_user', User::newFromName( $params['excludeuser'], false ) );
->getWhere( $db, 'rev_user', $params['excludeuser'] );
$this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
}
@ -265,6 +266,8 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
$ret = parent::getAllowedParams() + [
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'namespace' => [
ApiBase::PARAM_ISMULTI => true,
@ -287,6 +290,8 @@ class ApiQueryAllRevisions extends ApiQueryRevisionsBase {
],
'excludeuser' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'continue' => [
ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',

View file

@ -20,6 +20,9 @@
* @file
*/
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef\IntegerDef;
/**
* This is a three-in-one module to query:
* * backlinks - links pointing to the given page,
@ -352,8 +355,15 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$this->params['limit'] = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
$result->addParsedLimit( $this->getModuleName(), $this->params['limit'] );
} else {
$this->params['limit'] = (int)$this->params['limit'];
$this->validateLimit( 'limit', $this->params['limit'], 1, $userMax, $botMax );
$this->params['limit'] = $this->getMain()->getParamValidator()->validateValue(
$this, 'limit', (int)$this->params['limit'], [
ParamValidator::PARAM_TYPE => 'limit',
IntegerDef::PARAM_MIN => 1,
IntegerDef::PARAM_MAX => $userMax,
IntegerDef::PARAM_MAX2 => $botMax,
IntegerDef::PARAM_IGNORE_RANGE => true,
]
);
}
$this->rootTitle = $this->getTitleFromTitleOrPageId( $this->params );

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use Wikimedia\IPUtils;
use Wikimedia\Rdbms\IResultWrapper;
@ -351,6 +352,7 @@ class ApiQueryBlocks extends ApiQueryBase {
],
'users' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'cidr' ],
ApiBase::PARAM_ISMULTI => true
],
'ip' => [

View file

@ -24,6 +24,7 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Storage\NameTableAccessException;
@ -120,14 +121,14 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
if ( $params['user'] !== null ) {
// Don't query by user ID here, it might be able to use the ar_usertext_timestamp index.
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false );
->getWhere( $db, 'ar_user', $params['user'], false );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( $actorQuery['conds'] );
} elseif ( $params['excludeuser'] !== null ) {
// Here there's no chance of using ar_usertext_timestamp.
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) );
->getWhere( $db, 'ar_user', $params['excludeuser'] );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
@ -277,10 +278,14 @@ class ApiQueryDeletedRevisions extends ApiQueryRevisionsBase {
],
'tag' => null,
'user' => [
ApiBase::PARAM_TYPE => 'user'
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'excludeuser' => [
ApiBase::PARAM_TYPE => 'user'
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'continue' => [
ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',

View file

@ -21,9 +21,12 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\NameTableAccessException;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef\IntegerDef;
/**
* Query module to enumerate all deleted revisions.
@ -146,7 +149,15 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$this->getResult()->addParsedLimit( $this->getModuleName(), $limit );
}
$this->validateLimit( 'limit', $limit, 1, $userMax, $botMax );
$limit = $this->getMain()->getParamValidator()->validateValue(
$this, 'limit', $limit, [
ParamValidator::PARAM_TYPE => 'limit',
IntegerDef::PARAM_MIN => 1,
IntegerDef::PARAM_MAX => $userMax,
IntegerDef::PARAM_MAX2 => $botMax,
IntegerDef::PARAM_IGNORE_RANGE => true,
]
);
if ( $fld_token ) {
// Undelete tokens are identical for all pages, so we cache one here
@ -181,14 +192,14 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
if ( $params['user'] !== null ) {
// Don't query by user ID here, it might be able to use the ar_usertext_timestamp index.
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'ar_user', User::newFromName( $params['user'], false ), false );
->getWhere( $db, 'ar_user', $params['user'], false );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( $actorQuery['conds'] );
} elseif ( $params['excludeuser'] !== null ) {
// Here there's no chance of using ar_usertext_timestamp.
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'ar_user', User::newFromName( $params['excludeuser'], false ) );
->getWhere( $db, 'ar_user', $params['excludeuser'] );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
@ -442,10 +453,14 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
],
'tag' => null,
'user' => [
ApiBase::PARAM_TYPE => 'user'
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'excludeuser' => [
ApiBase::PARAM_TYPE => 'user'
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'prop' => [
ApiBase::PARAM_DFLT => 'user|comment',

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Storage\NameTableAccessException;
/**
@ -179,9 +180,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
if ( $user !== null ) {
// Note the joins in $q are the same as those from ->getJoin() above
// so we only need to add 'conds' here.
$q = $actorMigration->getWhere(
$db, 'log_user', User::newFromName( $params['user'], false )
);
$q = $actorMigration->getWhere( $db, 'log_user', $params['user'] );
$this->addWhere( $q['conds'] );
// T71222: MariaDB's optimizer, at least 10.1.37 and .38, likes to choose a wildly bad plan for
@ -447,6 +446,8 @@ class ApiQueryLogEvents extends ApiQueryBase {
],
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'title' => null,
'namespace' => [

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Storage\NameTableAccessException;
@ -272,7 +273,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
if ( $params['user'] !== null ) {
// Don't query by user ID here, it might be able to use the rc_user_text index.
$actorQuery = ActorMigration::newMigration()
->getWhere( $this->getDB(), 'rc_user', User::newFromName( $params['user'], false ), false );
->getWhere( $this->getDB(), 'rc_user', $params['user'], false );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( $actorQuery['conds'] );
@ -281,7 +282,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
if ( $params['excludeuser'] !== null ) {
// Here there's no chance to use the rc_user_text index, so allow ID to be used.
$actorQuery = ActorMigration::newMigration()
->getWhere( $this->getDB(), 'rc_user', User::newFromName( $params['excludeuser'], false ) );
->getWhere( $this->getDB(), 'rc_user', $params['excludeuser'] );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
@ -750,10 +751,14 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
ApiBase::PARAM_EXTRA_NAMESPACES => [ NS_MEDIA, NS_SPECIAL ],
],
'user' => [
ApiBase::PARAM_TYPE => 'user'
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'excludeuser' => [
ApiBase::PARAM_TYPE => 'user'
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'tag' => null,
'prop' => [

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Storage\NameTableAccessException;
@ -313,13 +314,13 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
if ( $params['user'] !== null ) {
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'rev_user', User::newFromName( $params['user'], false ) );
->getWhere( $db, 'rev_user', $params['user'] );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( $actorQuery['conds'] );
} elseif ( $params['excludeuser'] !== null ) {
$actorQuery = ActorMigration::newMigration()
->getWhere( $db, 'rev_user', User::newFromName( $params['excludeuser'], false ) );
->getWhere( $db, 'rev_user', $params['excludeuser'] );
$this->addTables( $actorQuery['tables'] );
$this->addJoinConds( $actorQuery['joins'] );
$this->addWhere( 'NOT(' . $actorQuery['conds'] . ')' );
@ -489,10 +490,14 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase {
],
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
],
'excludeuser' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
ApiBase::PARAM_HELP_MSG_INFO => [ [ 'singlepageonly' ] ],
],
'tag' => null,

View file

@ -25,6 +25,8 @@ use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\RevisionAccessException;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRecord;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef\IntegerDef;
/**
* A base class for functions common to producing a list of revisions.
@ -182,10 +184,15 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
}
}
if ( $this->limit === null ) {
$this->limit = 10;
}
$this->validateLimit( 'limit', $this->limit, 1, $userMax, $botMax );
$this->limit = $this->getMain()->getParamValidator()->validateValue(
$this, 'limit', $this->limit ?? 10, [
ParamValidator::PARAM_TYPE => 'limit',
IntegerDef::PARAM_MIN => 1,
IntegerDef::PARAM_MAX => $userMax,
IntegerDef::PARAM_MAX2 => $botMax,
IntegerDef::PARAM_IGNORE_RANGE => true,
]
);
$this->needSlots = $this->fetchContent || $this->fld_contentmodel ||
$this->fld_slotsize || $this->fld_slotsha1;

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Storage\NameTableAccessException;
@ -588,6 +589,7 @@ class ApiQueryUserContribs extends ApiQueryBase {
],
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'interwiki' ],
ApiBase::PARAM_ISMULTI => true
],
'userids' => [

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Revision\RevisionRecord;
/**
@ -447,9 +448,11 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
],
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
],
'excludeuser' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
],
'dir' => [
ApiBase::PARAM_DFLT => 'older',
@ -510,7 +513,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
ApiBase::PARAM_TYPE => RecentChange::getChangeTypes()
],
'owner' => [
ApiBase::PARAM_TYPE => 'user'
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
],
'token' => [
ApiBase::PARAM_TYPE => 'string',

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
/**
* This query action allows clients to retrieve a list of pages
@ -181,7 +182,8 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
]
],
'owner' => [
ApiBase::PARAM_TYPE => 'user'
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
],
'token' => [
ApiBase::PARAM_TYPE => 'string',

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
/**
* Reset password, with AuthManager
@ -101,6 +102,7 @@ class ApiResetPassword extends ApiBase {
$ret = [
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
],
'email' => [
ApiBase::PARAM_TYPE => 'string',

View file

@ -20,6 +20,8 @@
* @file
*/
use MediaWiki\ParamValidator\TypeDef\UserDef;
/**
* @ingroup API
*/
@ -117,6 +119,8 @@ class ApiRollback extends ApiBase {
],
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
UserDef::PARAM_RETURN_OBJECT => true,
ApiBase::PARAM_REQUIRED => true
],
'summary' => '',
@ -151,13 +155,7 @@ class ApiRollback extends ApiBase {
return $this->mUser;
}
// We need to be able to revert IPs, but getCanonicalName rejects them
$this->mUser = User::isIP( $params['user'] )
? $params['user']
: User::getCanonicalName( $params['user'] );
if ( !$this->mUser ) {
$this->dieWithError( [ 'apierror-invaliduser', wfEscapeWikiText( $params['user'] ) ] );
}
$this->mUser = $params['user'];
return $this->mUser;
}

View file

@ -21,6 +21,7 @@
*/
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\ParamValidator\TypeDef\UserDef;
/**
* API module that facilitates the unblocking of users. Requires API write mode
@ -109,9 +110,13 @@ class ApiUnblock extends ApiBase {
'id' => [
ApiBase::PARAM_TYPE => 'integer',
],
'user' => null,
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'cidr', 'id' ],
],
'userid' => [
ApiBase::PARAM_TYPE => 'integer'
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_DEPRECATED => true,
],
'reason' => '',
'tags' => [

View file

@ -34,9 +34,10 @@ class ApiUsageException extends MWException implements ILocalizedException {
* @param ApiBase|null $module API module responsible for the error, if known
* @param StatusValue $status Status holding errors
* @param int $httpCode HTTP error code to use
* @param Throwable|null $previous Previous exception
*/
public function __construct(
?ApiBase $module, StatusValue $status, $httpCode = 0
?ApiBase $module, StatusValue $status, $httpCode = 0, Throwable $previous = null
) {
if ( $status->isOK() ) {
throw new InvalidArgumentException( __METHOD__ . ' requires a fatal Status' );
@ -49,7 +50,7 @@ class ApiUsageException extends MWException implements ILocalizedException {
// customized by the local wiki.
$enMsg = clone $this->getApiMessage();
$enMsg->inLanguage( 'en' )->useDatabase( false );
parent::__construct( ApiErrorFormatter::stripMarkup( $enMsg->text() ), $httpCode );
parent::__construct( ApiErrorFormatter::stripMarkup( $enMsg->text() ), $httpCode, $previous );
}
/**
@ -58,15 +59,17 @@ class ApiUsageException extends MWException implements ILocalizedException {
* @param string|null $code See ApiMessage::create()
* @param array|null $data See ApiMessage::create()
* @param int $httpCode HTTP error code to use
* @param Throwable|null $previous Previous exception
* @return static
*/
public static function newWithMessage(
?ApiBase $module, $msg, $code = null, $data = null, $httpCode = 0
?ApiBase $module, $msg, $code = null, $data = null, $httpCode = 0, Throwable $previous = null
) {
return new static(
$module,
StatusValue::newFatal( ApiMessage::create( $msg, $code, $data ) ),
$httpCode
$httpCode,
$previous
);
}
@ -119,7 +122,8 @@ class ApiUsageException extends MWException implements ILocalizedException {
return get_class( $this ) . ": {$enMsg->getApiCode()}: {$text} "
. "in {$this->getFile()}:{$this->getLine()}\n"
. "Stack trace:\n{$this->getTraceAsString()}";
. "Stack trace:\n{$this->getTraceAsString()}"
. $this->getPrevious() ? "\n\nNext {$this->getPrevious()}" : "";
}
}

View file

@ -23,6 +23,8 @@
* @file
*/
use MediaWiki\ParamValidator\TypeDef\UserDef;
/**
* @ingroup API
*/
@ -170,9 +172,12 @@ class ApiUserrights extends ApiBase {
$a = [
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
'userid' => [
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_DEPRECATED => true,
],
'add' => [
ApiBase::PARAM_TYPE => $allGroups,

View file

@ -1,6 +1,7 @@
<?php
use MediaWiki\Auth\AuthManager;
use MediaWiki\ParamValidator\TypeDef\UserDef;
/**
* @ingroup API
@ -61,6 +62,7 @@ class ApiValidatePassword extends ApiBase {
],
'user' => [
ApiBase::PARAM_TYPE => 'user',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
],
'email' => null,
'realname' => null,

View file

@ -0,0 +1,249 @@
<?php
namespace MediaWiki\Api\Validator;
use ApiBase;
use ApiMain;
use ApiMessage;
use ApiUsageException;
use MediaWiki\Message\Converter as MessageConverter;
use MediaWiki\ParamValidator\TypeDef\NamespaceDef;
use MediaWiki\ParamValidator\TypeDef\TagsDef;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use Message;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\Message\MessageValue;
use Wikimedia\ObjectFactory;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef\EnumDef;
use Wikimedia\ParamValidator\TypeDef\IntegerDef;
use Wikimedia\ParamValidator\TypeDef\LimitDef;
use Wikimedia\ParamValidator\TypeDef\PasswordDef;
use Wikimedia\ParamValidator\TypeDef\PresenceBooleanDef;
use Wikimedia\ParamValidator\TypeDef\StringDef;
use Wikimedia\ParamValidator\TypeDef\TimestampDef;
use Wikimedia\ParamValidator\TypeDef\UploadDef;
use Wikimedia\ParamValidator\ValidationException;
/**
* This wraps a bunch of the API-specific parameter validation logic.
*
* It's intended to be used in ApiMain by composition.
*
* @since 1.35
* @ingroup API
*/
class ApiParamValidator {
/** @var ParamValidator */
private $paramValidator;
/** @var MessageConverter */
private $messageConverter;
/** Type defs for ParamValidator */
private const TYPE_DEFS = [
'boolean' => [ 'class' => PresenceBooleanDef::class ],
'enum' => [ 'class' => EnumDef::class ],
'integer' => [ 'class' => IntegerDef::class ],
'limit' => [ 'class' => LimitDef::class ],
'namespace' => [
'class' => NamespaceDef::class,
'services' => [ 'NamespaceInfo' ],
],
'NULL' => [
'class' => StringDef::class,
'args' => [ [
'allowEmptyWhenRequired' => true,
] ],
],
'password' => [ 'class' => PasswordDef::class ],
'string' => [ 'class' => StringDef::class ],
'submodule' => [ 'class' => SubmoduleDef::class ],
'tags' => [ 'class' => TagsDef::class ],
'text' => [ 'class' => StringDef::class ],
'timestamp' => [
'class' => TimestampDef::class,
'args' => [ [
'defaultFormat' => TS_MW,
] ],
],
'user' => [ 'class' => UserDef::class ],
'upload' => [ 'class' => UploadDef::class ],
];
/**
* @internal
* @param ApiMain $main
* @param ObjectFactory $objectFactory
*/
public function __construct( ApiMain $main, ObjectFactory $objectFactory ) {
$this->paramValidator = new ParamValidator(
new ApiParamValidatorCallbacks( $main ),
$objectFactory,
[
'typeDefs' => self::TYPE_DEFS,
'ismultiLimits' => [ ApiBase::LIMIT_SML1, ApiBase::LIMIT_SML2 ],
]
);
$this->messageConverter = new MessageConverter();
}
/**
* List known type names
* @return string[]
*/
public function knownTypes() : array {
return $this->paramValidator->knownTypes();
}
/**
* Adjust certain settings where ParamValidator differs from historical Action API behavior
* @param array|mixed $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] );
}
if ( isset( $settings[EnumDef::PARAM_DEPRECATED_VALUES] ) ) {
foreach ( $settings[EnumDef::PARAM_DEPRECATED_VALUES] as &$v ) {
if ( $v === null || $v === true || $v instanceof MessageValue ) {
continue;
}
// Convert the message specification to a DataMessageValue. Flag in the data
// that it was so converted, so ApiParamValidatorCallbacks::recordCondition() can
// take that into account.
// @phan-suppress-next-line PhanTypeMismatchArgument
$msg = $this->messageConverter->convertMessage( ApiMessage::create( $v ) );
$v = DataMessageValue::new(
$msg->getKey(),
$msg->getParams(),
'bogus',
[ '💩' => 'back-compat' ]
);
}
unset( $v );
}
return $settings;
}
/**
* Convert a ValidationException to an ApiUsageException
* @param ApiBase $module
* @param ValidationException $ex
* @throws ApiUsageException always
*/
private function convertValidationException( ApiBase $module, ValidationException $ex ) : array {
$mv = $ex->getFailureMessage();
throw ApiUsageException::newWithMessage(
$module,
$this->messageConverter->convertMessageValue( $mv ),
$mv->getCode(),
$mv->getData(),
0,
$ex
);
}
/**
* Get and validate a value
* @param ApiBase $module
* @param string $name Parameter name, unprefixed
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @param array $options Options array
* @return mixed Validated parameter value
* @throws ApiUsageException if the value is invalid
*/
public function getValue( ApiBase $module, string $name, $settings, array $options = [] ) {
$options['module'] = $module;
$name = $module->encodeParamName( $name );
$settings = $this->normalizeSettings( $settings );
try {
return $this->paramValidator->getValue( $name, $settings, $options );
} catch ( ValidationException $ex ) {
$this->convertValidationException( $module, $ex );
}
}
/**
* Valiate a parameter value using a settings array
*
* @param ApiBase $module
* @param string $name Parameter name, unprefixed
* @param mixed $value Parameter value
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @param array $options Options array
* @return mixed Validated parameter value(s)
* @throws ApiUsageException if the value is invalid
*/
public function validateValue(
ApiBase $module, string $name, $value, $settings, array $options = []
) {
$options['module'] = $module;
$name = $module->encodeParamName( $name );
$settings = $this->normalizeSettings( $settings );
try {
return $this->paramValidator->validateValue( $name, $value, $settings, $options );
} catch ( ValidationException $ex ) {
$this->convertValidationException( $module, $ex );
}
}
/**
* Describe parameter settings in a machine-readable format.
*
* @param ApiBase $module
* @param string $name Parameter name.
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @param array $options Options array.
* @return array
*/
public function getParamInfo( ApiBase $module, string $name, $settings, array $options ) : array {
$options['module'] = $module;
$name = $module->encodeParamName( $name );
return $this->paramValidator->getParamInfo( $name, $settings, $options );
}
/**
* Describe parameter settings in human-readable format
*
* @param ApiBase $module
* @param string $name Parameter name being described.
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @param array $options Options array.
* @return Message[]
*/
public function getHelpInfo( ApiBase $module, string $name, $settings, array $options ) : array {
$options['module'] = $module;
$name = $module->encodeParamName( $name );
$ret = $this->paramValidator->getHelpInfo( $name, $settings, $options );
foreach ( $ret as &$m ) {
$k = $m->getKey();
$m = $this->messageConverter->convertMessageValue( $m );
if ( substr( $k, 0, 20 ) === 'paramvalidator-help-' ) {
$m = new Message(
[ 'api-help-param-' . substr( $k, 20 ), $k ],
$m->getParams()
);
}
}
'@phan-var Message[] $ret'; // The above loop converts it
return $ret;
}
}

View file

@ -0,0 +1,136 @@
<?php
namespace MediaWiki\Api\Validator;
use ApiMain;
use MediaWiki\Message\Converter as MessageConverter;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\Callbacks;
use Wikimedia\ParamValidator\Util\UploadedFile;
/**
* ParamValidator callbacks for the Action API
* @since 1.35
* @ingroup API
*/
class ApiParamValidatorCallbacks implements Callbacks {
/** @var ApiMain */
private $apiMain;
/** @var MessageConverter */
private $messageConverter;
/**
* @internal
* @param ApiMain $main
*/
public function __construct( ApiMain $main ) {
$this->apiMain = $main;
$this->messageConverter = new MessageConverter();
}
public function hasParam( $name, array $options ) {
return $this->apiMain->getCheck( $name );
}
public function getValue( $name, $default, array $options ) {
$value = $this->apiMain->getVal( $name, $default );
$request = $this->apiMain->getRequest();
$rawValue = $request->getRawVal( $name );
if ( is_string( $rawValue ) ) {
// Preserve U+001F for multi-values
if ( substr( $rawValue, 0, 1 ) === "\x1f" ) {
// This loses the potential checkTitleEncoding() transformation done by
// WebRequest for $_GET. Let's call that a feature.
$value = implode( "\x1f", $request->normalizeUnicode( explode( "\x1f", $rawValue ) ) );
}
// Check for NFC normalization, and warn
if ( $rawValue !== $value ) {
$options['module']->handleParamNormalization( $name, $value, $rawValue );
}
}
return $value;
}
public function hasUpload( $name, array $options ) {
return $this->getUploadedFile( $name, $options ) !== null;
}
public function getUploadedFile( $name, array $options ) {
$upload = $this->apiMain->getUpload( $name );
if ( !$upload->exists() ) {
return null;
}
return new UploadedFile( [
'error' => $upload->getError(),
'tmp_name' => $upload->getTempName(),
'size' => $upload->getSize(),
'name' => $upload->getName(),
'type' => $upload->getType(),
] );
}
public function recordCondition(
DataMessageValue $message, $name, $value, array $settings, array $options
) {
$module = $options['module'];
$code = $message->getCode();
switch ( $code ) {
case 'param-deprecated': // @codeCoverageIgnore
case 'deprecated-value': // @codeCoverageIgnore
if ( $code === 'param-deprecated' ) {
$feature = $name;
} else {
$feature = $name . '=' . $value;
$data = $message->getData() ?? [];
if ( isset( $data['💩'] ) ) {
// This is from an old-style Message. Strip out ParamValidator's added params.
unset( $data['💩'] );
$message = DataMessageValue::new(
$message->getKey(),
array_slice( $message->getParams(), 2 ),
$code,
$data
);
}
}
$m = $module;
while ( !$m->isMain() ) {
$p = $m->getParent();
$mName = $m->getModuleName();
$mParam = $p->encodeParamName( $p->getModuleManager()->getModuleGroup( $mName ) );
$feature = "{$mParam}={$mName}&{$feature}";
$m = $p;
}
$module->addDeprecation(
$this->messageConverter->convertMessageValue( $message ),
$feature,
$message->getData()
);
break;
case 'param-sensitive': // @codeCoverageIgnore
$module->getMain()->markParamsSensitive( $name );
break;
default:
$module->addWarning(
$this->messageConverter->convertMessageValue( $message ),
$message->getCode(),
$message->getData()
);
break;
}
}
public function useHighLimits( array $options ) {
return $this->apiMain->canApiHighLimits();
}
}

View file

@ -0,0 +1,172 @@
<?php
namespace MediaWiki\Api\Validator;
use ApiBase;
use ApiUsageException;
use Html;
use Wikimedia\ParamValidator\TypeDef\EnumDef;
/**
* Type definition for submodule types
*
* A submodule type is an enum type for selecting Action API submodules.
*
* @since 1.35
*/
class SubmoduleDef extends EnumDef {
/**
* (string[]) Map parameter values to submodule paths.
*
* Default is to use all modules in $options['module']->getModuleManager()
* in the group matching the parameter name.
*/
public const PARAM_SUBMODULE_MAP = 'param-submodule-map';
/**
* (string) Used to indicate the 'g' prefix added by ApiQueryGeneratorBase
* (and similar if anything else ever does that).
*/
public const PARAM_SUBMODULE_PARAM_PREFIX = 'param-submodule-param-prefix';
public function getEnumValues( $name, array $settings, array $options ) {
if ( isset( $settings[self::PARAM_SUBMODULE_MAP] ) ) {
$modules = array_keys( $settings[self::PARAM_SUBMODULE_MAP] );
} else {
$modules = $options['module']->getModuleManager()->getNames( $name );
}
return $modules;
}
public function getParamInfo( $name, array $settings, array $options ) {
$info = parent::getParamInfo( $name, $settings, $options );
$module = $options['module'];
if ( isset( $settings[self::PARAM_SUBMODULE_MAP] ) ) {
$info['type'] = array_keys( $settings[self::PARAM_SUBMODULE_MAP] );
$info['submodules'] = $settings[self::PARAM_SUBMODULE_MAP];
} else {
$info['type'] = $module->getModuleManager()->getNames( $name );
$prefix = $module->isMain() ? '' : ( $module->getModulePath() . '+' );
$info['submodules'] = [];
foreach ( $info['type'] as $v ) {
$info['submodules'][$v] = $prefix . $v;
}
}
if ( isset( $settings[self::PARAM_SUBMODULE_PARAM_PREFIX] ) ) {
$info['submoduleparamprefix'] = $settings[self::PARAM_SUBMODULE_PARAM_PREFIX];
}
$submoduleFlags = []; // for sorting: higher flags are sorted later
$submoduleNames = []; // for sorting: lexicographical, ascending
foreach ( $info['submodules'] as $v => $submodulePath ) {
try {
$submod = $module->getModuleFromPath( $submodulePath );
} catch ( ApiUsageException $ex ) {
$submoduleFlags[] = 0;
$submoduleNames[] = $v;
continue;
}
$flags = 0;
if ( $submod && $submod->isDeprecated() ) {
$info['deprecatedvalues'][] = $v;
$flags |= 1;
}
if ( $submod && $submod->isInternal() ) {
$info['internalvalues'][] = $v;
$flags |= 2;
}
$submoduleFlags[] = $flags;
$submoduleNames[] = $v;
}
// sort $info['submodules'] and $info['type'] by $submoduleFlags and $submoduleNames
array_multisort( $submoduleFlags, $submoduleNames, $info['submodules'], $info['type'] );
if ( isset( $info['deprecatedvalues'] ) ) {
sort( $info['deprecatedvalues'] );
}
if ( isset( $info['internalvalues'] ) ) {
sort( $info['internalvalues'] );
}
return $info;
}
private function getSubmoduleMap( ApiBase $module, string $name, array $settings ) : array {
if ( isset( $settings[self::PARAM_SUBMODULE_MAP] ) ) {
$map = $settings[self::PARAM_SUBMODULE_MAP];
} else {
$prefix = $module->isMain() ? '' : ( $module->getModulePath() . '+' );
$map = [];
foreach ( $module->getModuleManager()->getNames( $name ) as $submoduleName ) {
$map[$submoduleName] = $prefix . $submoduleName;
}
}
return $map;
}
protected function sortEnumValues(
string $name, array $values, array $settings, array $options
) : array {
$module = $options['module'];
$map = $this->getSubmoduleMap( $module, $name, $settings );
$submoduleFlags = []; // for sorting: higher flags are sorted later
foreach ( $values as $k => $v ) {
$flags = 0;
try {
$submod = isset( $map[$v] ) ? $module->getModuleFromPath( $map[$v] ) : null;
if ( $submod && $submod->isDeprecated() ) {
$flags |= 1;
}
if ( $submod && $submod->isInternal() ) {
$flags |= 2;
}
} catch ( ApiUsageException $ex ) {
// Ignore
}
$submoduleFlags[$k] = $flags;
}
array_multisort( $submoduleFlags, $values, SORT_NATURAL );
return $values;
}
protected function getEnumValuesForHelp( $name, array $settings, array $options ) {
$module = $options['module'];
$map = $this->getSubmoduleMap( $module, $name, $settings );
$defaultAttrs = [ 'dir' => 'ltr', 'lang' => 'en' ];
$values = [];
$submoduleFlags = []; // for sorting: higher flags are sorted later
$submoduleNames = []; // for sorting: lexicographical, ascending
foreach ( $map as $v => $m ) {
$attrs = $defaultAttrs;
$flags = 0;
try {
$submod = $module->getModuleFromPath( $m );
if ( $submod && $submod->isDeprecated() ) {
$attrs['class'][] = 'apihelp-deprecated-value';
$flags |= 1;
}
if ( $submod && $submod->isInternal() ) {
$attrs['class'][] = 'apihelp-internal-value';
$flags |= 2;
}
} catch ( ApiUsageException $ex ) {
// Ignore
}
$v = Html::element( 'span', $attrs, $v );
$values[] = "[[Special:ApiHelp/{$m}|{$v}]]";
$submoduleFlags[] = $flags;
$submoduleNames[] = $v;
}
// sort $values by $submoduleFlags and $submoduleNames
array_multisort( $submoduleFlags, $submoduleNames, SORT_NATURAL, $values, SORT_NATURAL );
return $values;
}
}

View file

@ -27,8 +27,8 @@
"apihelp-main-param-errorsuselocal": "If given, error texts will use locally-customized messages from the {{ns:MediaWiki}} namespace.",
"apihelp-block-summary": "Block a user.",
"apihelp-block-param-user": "Username, IP address, or IP address range to block. Cannot be used together with <var>$1userid</var>",
"apihelp-block-param-userid": "User ID to block. Cannot be used together with <var>$1user</var>.",
"apihelp-block-param-user": "User to block.",
"apihelp-block-param-userid": "Specify <kbd>$1user=#<var>ID</var></kbd> instead.",
"apihelp-block-param-expiry": "Expiry time. May be relative (e.g. <kbd>5 months</kbd> or <kbd>2 weeks</kbd>) or absolute (e.g. <kbd>2014-09-18T12:34:56Z</kbd>). If set to <kbd>infinite</kbd>, <kbd>indefinite</kbd>, or <kbd>never</kbd>, the block will never expire.",
"apihelp-block-param-reason": "Reason for block.",
"apihelp-block-param-anononly": "Block anonymous users only (i.e. disable anonymous edits for this IP address).",
@ -1500,9 +1500,9 @@
"apihelp-tokens-example-emailmove": "Retrieve an email token and a move token.",
"apihelp-unblock-summary": "Unblock a user.",
"apihelp-unblock-param-id": "ID of the block to unblock (obtained through <kbd>list=blocks</kbd>). Cannot be used together with <var>$1user</var> or <var>$1userid</var>.",
"apihelp-unblock-param-user": "Username, IP address or IP address range to unblock. Cannot be used together with <var>$1id</var> or <var>$1userid</var>.",
"apihelp-unblock-param-userid": "User ID to unblock. Cannot be used together with <var>$1id</var> or <var>$1user</var>.",
"apihelp-unblock-param-id": "ID of the block to unblock (obtained through <kbd>list=blocks</kbd>). Cannot be used together with <var>$1user</var>.",
"apihelp-unblock-param-user": "User to unblock. Cannot be used together with <var>$1id</var>.",
"apihelp-unblock-param-userid": "Specify <kbd>$1user=#<var>ID</var></kbd> instead.",
"apihelp-unblock-param-reason": "Reason for unblock.",
"apihelp-unblock-param-tags": "Change tags to apply to the entry in the block log.",
"apihelp-unblock-example-id": "Unblock block ID #<kbd>105</kbd>.",
@ -1545,8 +1545,8 @@
"apihelp-upload-example-filekey": "Complete an upload that failed due to warnings.",
"apihelp-userrights-summary": "Change a user's group membership.",
"apihelp-userrights-param-user": "User name.",
"apihelp-userrights-param-userid": "User ID.",
"apihelp-userrights-param-user": "User.",
"apihelp-userrights-param-userid": "Specify <kbd>$1user=#<var>ID</var></kbd> instead.",
"apihelp-userrights-param-add": "Add the user to these groups, or if they are already a member, update the expiry of their membership in that group.",
"apihelp-userrights-param-expiry": "Expiry timestamps. May be relative (e.g. <kbd>5 months</kbd> or <kbd>2 weeks</kbd>) or absolute (e.g. <kbd>2014-09-18T12:34:56Z</kbd>). If only one timestamp is set, it will be used for all groups passed to the <var>$1add</var> parameter. Use <kbd>infinite</kbd>, <kbd>indefinite</kbd>, <kbd>infinity</kbd>, or <kbd>never</kbd> for a never-expiring user group.",
"apihelp-userrights-param-remove": "Remove the user from these groups.",
@ -1629,33 +1629,21 @@
"api-help-parameters": "{{PLURAL:$1|Parameter|Parameters}}:",
"api-help-param-deprecated": "Deprecated.",
"api-help-param-internal": "Internal.",
"api-help-param-required": "This parameter is required.",
"api-help-param-templated": "This is a [[Special:ApiHelp/main#main/templatedparams|templated parameter]]. When making the request, $2.",
"api-help-param-templated-var-first": "<var>&#x7B;$1&#x7D;</var> in the parameter's name should be replaced with values of <var>$2</var>",
"api-help-param-templated-var": "<var>&#x7B;$1&#x7D;</var> with values of <var>$2</var>",
"api-help-datatypes-header": "Data types",
"api-help-datatypes": "Input to MediaWiki should be NFC-normalized UTF-8. MediaWiki may attempt to convert other input, but this may cause some operations (such as [[Special:ApiHelp/edit|edits]] with MD5 checks) to fail.\n\nSome parameter types in API requests need further explanation:\n;boolean\n:Boolean parameters work like HTML checkboxes: if the parameter is specified, regardless of value, it is considered true. For a false value, omit the parameter entirely.\n;timestamp\n:Timestamps may be specified in several formats, see [[mw:Special:MyLanguage/Timestamp|the Timestamp library input formats documented on mediawiki.org]] for details. ISO 8601 date and time is recommended: <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd>. Additionally, the string <kbd>now</kbd> may be used to specify the current timestamp.\n;alternative multiple-value separator\n:Parameters that take multiple values are normally submitted with the values separated using the pipe character, e.g. <kbd>param=value1|value2</kbd> or <kbd>param=value1%7Cvalue2</kbd>. If a value must contain the pipe character, use U+001F (Unit Separator) as the separator ''and'' prefix the value with U+001F, e.g. <kbd>param=%1Fvalue1%1Fvalue2</kbd>.",
"api-help-datatypes-top": "Input to MediaWiki should be NFC-normalized UTF-8. MediaWiki may attempt to convert other input, but this may cause some operations (such as [[Special:ApiHelp/edit|edits]] with MD5 checks) to fail.\n\nParameters that take multiple values are normally submitted with the values separated using the pipe character, e.g. <kbd>param=value1|value2</kbd> or <kbd>param=value1%7Cvalue2</kbd>. If a value must contain the pipe character, use U+001F (Unit Separator) as the separator ''and'' prefix the value with U+001F, e.g. <kbd>param=%1Fvalue1%1Fvalue2</kbd>.\n\nSome parameter types in API requests need further explanation:",
"api-help-datatype-boolean": "Boolean parameters work like HTML checkboxes: if the parameter is specified, regardless of value, it is considered true. For a false value, omit the parameter entirely.",
"api-help-datatype-timestamp": "Timestamps may be specified in several formats, see [[mw:Special:MyLanguage/Timestamp|the Timestamp library input formats documented on mediawiki.org]] for details. ISO 8601 date and time is recommended: <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd>. Additionally, the string <kbd>now</kbd> may be used to specify the current timestamp.",
"api-help-templatedparams-header": "Templated parameters",
"api-help-templatedparams": "Templated parameters support cases where an API module needs a value for each value of some other parameter. For example, if there were an API module to request fruit, it might have a parameter <var>fruits</var> to specify which fruits are being requested and a templated parameter <var>{fruit}-quantity</var> to specify how many of each fruit to request. An API client that wants 1 apple, 5 bananas, and 20 strawberries could then make a request like <kbd>fruits=apples|bananas|strawberries&apples-quantity=1&bananas-quantity=5&strawberries-quantity=20</kbd>.",
"api-help-param-type-limit": "Type: integer or <kbd>max</kbd>",
"api-help-param-type-integer": "Type: {{PLURAL:$1|1=integer|2=list of integers}}",
"api-help-param-type-boolean": "Type: boolean ([[Special:ApiHelp/main#main/datatypes|details]])",
"api-help-param-type-password": "",
"api-help-param-type-timestamp": "Type: {{PLURAL:$1|1=timestamp|2=list of timestamps}} ([[Special:ApiHelp/main#main/datatypes|allowed formats]])",
"api-help-param-type-user": "Type: {{PLURAL:$1|1=user name|2=list of user names}}",
"api-help-param-list": "{{PLURAL:$1|1=One of the following values|2=Values (separate with <kbd>{{!}}</kbd> or [[Special:ApiHelp/main#main/datatypes|alternative]])}}: $2",
"api-help-param-list-can-be-empty": "{{PLURAL:$1|0=Must be empty|Can be empty, or $2}}",
"api-help-param-limit": "No more than $1 allowed.",
"api-help-param-limit2": "No more than $1 ($2 for bots) allowed.",
"api-help-param-integer-min": "The {{PLURAL:$1|1=value|2=values}} must be no less than $2.",
"api-help-param-integer-max": "The {{PLURAL:$1|1=value|2=values}} must be no greater than $3.",
"api-help-param-integer-minmax": "The {{PLURAL:$1|1=value|2=values}} must be between $2 and $3.",
"api-help-param-upload": "Must be posted as a file upload using multipart/form-data.",
"api-help-param-type-presenceboolean": "Type: boolean ([[Special:ApiHelp/main#main/datatype/boolean|details]])",
"api-help-param-type-timestamp": "Type: {{PLURAL:$1|1=timestamp|2=list of timestamps}} ([[Special:ApiHelp/main#main/datatype/timestamp|allowed formats]])",
"api-help-param-type-enum": "{{PLURAL:$1|1=One of the following values|2=Values (separate with <kbd>{{!}}</kbd> or [[Special:ApiHelp/main#main/datatypes|alternative]])}}: $2",
"api-help-param-multi-separate": "Separate values with <kbd>|</kbd> or [[Special:ApiHelp/main#main/datatypes|alternative]].",
"api-help-param-multi-max": "Maximum number of values is {{PLURAL:$1|$1}} ({{PLURAL:$2|$2}} for bots).",
"api-help-param-multi-max-simple": "Maximum number of values is {{PLURAL:$1|$1}}.",
"api-help-param-multi-all": "To specify all values, use <kbd>$1</kbd>.",
"api-help-param-default": "Default: $1",
"api-help-param-default-empty": "Default: <span class=\"apihelp-empty\">(empty)</span>",
"api-help-param-token": "A \"$1\" token retrieved from [[Special:ApiHelp/query+tokens|action=query&meta=tokens]]",
"api-help-param-token-webui": "For compatibility, the token used in the web UI is also accepted.",
@ -1664,8 +1652,6 @@
"api-help-param-direction": "In which direction to enumerate:\n;newer:List oldest first. Note: $1start has to be before $1end.\n;older:List newest first (default). Note: $1start has to be later than $1end.",
"api-help-param-continue": "When more results are available, use this to continue.",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(no description)</span>",
"api-help-param-maxbytes": "Cannot be longer than $1 {{PLURAL:$1|byte|bytes}}.",
"api-help-param-maxchars": "Cannot be longer than $1 {{PLURAL:$1|character|characters}}.",
"api-help-examples": "{{PLURAL:$1|Example|Examples}}:",
"api-help-permissions": "{{PLURAL:$1|Permission|Permissions}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|Granted to}}: $2",

View file

@ -1519,34 +1519,22 @@
"api-help-parameters": "Label for the API help parameters section\n\nParameters:\n* $1 - Number of parameters to be displayed\n{{Identical|Parameter}}",
"api-help-param-deprecated": "Displayed in the API help for any deprecated parameter\n{{Identical|Deprecated}}",
"api-help-param-internal": "Displayed in the API help for any internal parameter",
"api-help-param-required": "Displayed in the API help for any required parameter",
"api-help-param-templated": "Displayed in the API help for any templated parameter.\n\nParameters:\n* $1 - Count of template variables in the parameter name.\n* $2 - A list, composed using {{msg-mw|comma-separator}} and {{msg-mw|and}}, of the template variables in the parameter name. The first is formatted using {{msg-mw|api-help-param-templated-var-first|notext=1}} and the rest use {{msg-mw|api-help-param-templated-var|notext=1}}.\n\nSee also:\n* {{msg-mw|api-help-param-templated-var-first}}\n* {{msg-mw|api-help-param-templated-var}}",
"api-help-param-templated-var-first": "Used with {{msg-mw|api-help-param-templated|notext=1}} to display templated parameter replacement variables. See that message for context.\n\nParameters:\n* $1 - Variable.\n* $2 - Parameter from which values are taken.\n\nSee also:\n* {{msg-mw|api-help-param-templated}}\n* {{msg-mw|api-help-param-templated-var}}",
"api-help-param-templated-var": "Used with {{msg-mw|api-help-param-templated|notext=1}} to display templated parameter replacement variables. See that message for context.\n\nParameters:\n* $1 - Variable.\n* $2 - Parameter from which values are taken.\n\nSee also:\n* {{msg-mw|api-help-param-templated}}\n* {{msg-mw|api-help-param-templated-var-first}}",
"api-help-datatypes-header": "Header for the data type section in the API help output",
"api-help-datatypes": "{{technical}} {{doc-important|Do not translate or reformat dates inside <nowiki><kbd></kbd></nowiki> or <nowiki><var></var></nowiki> tags}} Documentation of certain API data types\nSee also:\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]",
"api-help-datatypes-top": "{{technical}} {{doc-important|Do not translate or reformat dates inside <nowiki><kbd></kbd></nowiki> or <nowiki><var></var></nowiki> tags}} General documentation of API data types\nSee also:\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]",
"api-help-datatype-boolean": "{{technical}} {{doc-important|Do not translate or reformat dates inside <nowiki><kbd></kbd></nowiki> or <nowiki><var></var></nowiki> tags}} Documentation of API boolean data type\nSee also:\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]",
"api-help-datatype-timestamp": "{{technical}} {{doc-important|Do not translate or reformat dates inside <nowiki><kbd></kbd></nowiki> or <nowiki><var></var></nowiki> tags}} Documentation of API timestamp data type\nSee also:\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]",
"api-help-templatedparams-header": "Header for the \"templated parameters\" section in the API help output.",
"api-help-templatedparams": "{{technical}} {{doc-important|Unlike in other API messages, feel free to localize the words \"fruit\", \"fruits\", \"quantity\", \"apples\", \"bananas\", and \"strawberries\" in this message even when inside <nowiki><kbd></kbd></nowiki> or <nowiki><var></var></nowiki> tags. Do not change the punctuation, only the words.}} Documentation for the \"templated parameters\" feature.",
"api-help-param-type-limit": "{{technical}} {{doc-important|Do not translate text inside &lt;kbd&gt; tags}} Used to indicate that a parameter is a \"limit\" type.\n\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n{{Related|Api-help-param-type}}",
"api-help-param-type-integer": "{{technical}} Used to indicate that a parameter is an integer or list of integers. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes a list of values.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n{{Related|Api-help-param-type}}",
"api-help-param-type-boolean": "{{technical}} {{doc-important|Do not translate <code>Special:ApiHelp</code> in this message.}} Used to indicate that a parameter is a boolean. Parameters:\n* $1 - Always 1.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n{{Related|Api-help-param-type}}",
"api-help-param-type-password": "{{ignored}}{{technical}} Used to indicate that a parameter is a password or list of passwords. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes a list of values.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]",
"api-help-param-type-timestamp": "{{technical}} {{doc-important|Do not translate <code>Special:ApiHelp</code> in this message.}} Used to indicate that a parameter is a timestamp or list of timestamps. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes a list of values.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n{{Related|Api-help-param-type}}",
"api-help-param-type-user": "{{technical}} Used to indicate that a parameter is a username or list of usernames. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes a list of values.\nSee also:\n* {{msg-mw|api-help-datatypes}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n{{Related|Api-help-param-type}}",
"api-help-param-list": "Used to display the possible values for a parameter taking a list of values\n\nParameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - Comma-separated list of values, possibly formatted using {{msg-mw|api-help-param-list-can-be-empty}}\n{{Identical|Value}}",
"api-help-param-list-can-be-empty": "Used to indicate that one of the possible values in the list is the empty string.\n\nParameters:\n* $1 - Number of items in the rest of the list; may be 0\n* $2 - Remainder of the list as a comma-separated string",
"api-help-param-limit": "Used to display the maximum value of a limit parameter\n\nParameters:\n* $1 - Maximum value",
"api-help-param-limit2": "Used to display the maximum values of a limit parameter\n\nParameters:\n* $1 - Maximum value without the apihighlimits right\n* $2 - Maximum value with the apihighlimits right",
"api-help-param-integer-min": "Used to display an integer parameter with a minimum but no maximum value\n\nParameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - Minimum value\n* $3 - unused\n\nSee also:\n* {{msg-mw|api-help-param-integer-max}}\n* {{msg-mw|api-help-param-integer-minmax}}",
"api-help-param-integer-max": "Used to display an integer parameter with a maximum but no minimum value.\n\nParameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - (Unused)\n* $3 - Maximum value\nSee also:\n* {{msg-mw|Api-help-param-integer-min}}\n* {{msg-mw|Api-help-param-integer-minmax}}",
"api-help-param-integer-minmax": "Used to display an integer parameter with a maximum and minimum values\n\nParameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - Minimum value\n* $3 - Maximum value\n\nSee also:\n* {{msg-mw|api-help-param-integer-min}}\n* {{msg-mw|api-help-param-integer-max}}",
"api-help-param-upload": "{{technical}} Used to indicate that an 'upload'-type parameter must be posted as a file upload using multipart/form-data",
"api-help-param-multi-separate": "Used to indicate how to separate multiple values. Not used with {{msg-mw|api-help-param-list}}.",
"api-help-param-multi-max": "Used to indicate the maximum number of values accepted for a multi-valued parameter when that value is influenced by the user having apihighlimits right (otherwise {{msg-mw|api-help-param-multi-max-simple}} is used).\n\nParameters:\n* $1 - Maximum value without the apihighlimits right\n* $2 - Maximum value with the apihighlimits right",
"api-help-param-multi-max-simple": "Used to indicate the maximum number of values accepted for a multi-valued parameter when that value is not influenced by the user having apihighlimits right (otherwise {{msg-mw|api-help-param-multi-max}} is used).\n\nParameters:\n* $1 - Maximum value",
"api-help-param-type-limit": "{{technical}} {{doc-important|Do not translate text inside &lt;kbd&gt; tags}} Used to indicate that a parameter is a \"limit\" type. Parameters:\n* $1 - Always 1.\nSee also:\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n* [[Special:PrefixIndex/MediaWiki:paramvalidator-help-type]]\n{{Related|Api-help-param-type}}",
"api-help-param-type-presenceboolean": "{{technical}} {{doc-important|Do not translate <code>Special:ApiHelp</code> in this message.}} Used to indicate that a parameter is a boolean. Parameters:\n* $1 - Always 1.\nSee also:\n* {{msg-mw|api-help-datatype-boolean}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n* [[Special:PrefixIndex/MediaWiki:paramvalidator-help-type]]\n{{Related|Api-help-param-type}}",
"api-help-param-type-timestamp": "{{technical}} {{doc-important|Do not translate <code>Special:ApiHelp</code> in this message.}} Used to indicate that a parameter is a timestamp or list of timestamps. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes a list of values.\nSee also:\n* {{msg-mw|api-help-datatype-timestamp}}\n* [[Special:PrefixIndex/MediaWiki:api-help-param-type]]\n* [[Special:PrefixIndex/MediaWiki:paramvalidator-help-type]]\n{{Related|Api-help-param-type}}",
"api-help-param-type-enum": "Used to display the possible values for a parameter taking a list of values\n\nParameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - Comma-separated list of values, possibly formatted using {{msg-mw|paramvalidator-help-type-enum-can-be-empty}}\n{{Identical|Value}}\n{{Related|Api-help-param-type}}",
"api-help-param-multi-separate": "Used to indicate how to separate multiple values. Not used with {{msg-mw|api-help-param-type-enum}}.",
"api-help-param-multi-all": "Used to indicate what string can be used to specify all possible values of a multi-valued parameter. \n\nParameters:\n* $1 - String to specify all possible values of the parameter",
"api-help-param-default": "Used to display the default value for an API parameter\n\nParameters:\n* $1 - Default value\n\nSee also:\n* {{msg-mw|api-help-param-default-empty}}\n{{Identical|Default}}",
"api-help-param-default-empty": "Used to display the default value for an API parameter when that default is an empty value\n\nSee also:\n* {{msg-mw|api-help-param-default}}",
"api-help-param-default-empty": "Used to display the default value for an API parameter when that default is an empty value\n\nSee also:\n* {{msg-mw|paramvalidator-help-default}}",
"api-help-param-token": "{{doc-apihelp-param|description=any 'token' parameter|paramstart=2|params=\n* $1 - Token type|noseealso=1}}",
"api-help-param-token-webui": "{{doc-apihelp-param|description=additional text for any \"token\" parameter, explaining that web UI tokens are also accepted|noseealso=1}}",
"api-help-param-disabled-in-miser-mode": "{{doc-apihelp-param|description=any parameter that is disabled when [[mw:Manual:$wgMiserMode|$wgMiserMode]] is set.|noseealso=1}}",
@ -1554,8 +1542,6 @@
"api-help-param-direction": "{{doc-apihelp-param|description=any standard \"dir\" parameter|noseealso=1}}",
"api-help-param-continue": "{{doc-apihelp-param|description=any standard \"continue\" parameter, or other parameter with the same semantics|noseealso=1}}",
"api-help-param-no-description": "Displayed on API parameters that lack any description",
"api-help-param-maxbytes": "Used to display the maximum allowed length of a parameter, in bytes.",
"api-help-param-maxchars": "Used to display the maximum allowed length of a parameter, in characters.",
"api-help-examples": "Label for the API help examples section\n\nParameters:\n* $1 - Number of examples to be displayed\n{{Identical|Example}}",
"api-help-permissions": "Label for the \"permissions\" section in the main module's help output.\n\nParameters:\n* $1 - Number of permissions displayed\n{{Identical|Permission}}",
"api-help-permissions-granted-to": "Used to introduce the list of groups each permission is assigned to.\n\nParameters:\n* $1 - Number of groups\n* $2 - List of group names, comma-separated",

View file

@ -504,9 +504,12 @@ class ChangeTags {
*/
protected static function restrictedTagError( $msgOne, $msgMulti, $tags ) {
$lang = RequestContext::getMain()->getLanguage();
$tags = array_values( $tags );
$count = count( $tags );
return Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
$status = Status::newFatal( ( $count > 1 ) ? $msgMulti : $msgOne,
$lang->commaList( $tags ), $count );
$status->value = $tags;
return $status;
}
/**

View file

@ -4278,5 +4278,12 @@
"mycustomjsredirectprotected": "You do not have permission to edit this JavaScript page because it is a redirect and it does not point inside your userspace.",
"deflate-invaliddeflate": "Content provided is not properly deflated",
"unprotected-js": "For security reasons JavaScript cannot be loaded from unprotected pages. Please only create javascript in the MediaWiki: namespace or as a User subpage",
"userlogout-continue": "Do you want to log out?"
"userlogout-continue": "Do you want to log out?",
"paramvalidator-baduser": "Invalid value \"$2\" for user parameter <var>$1</var>.",
"paramvalidator-help-type-user": "Type: {{PLURAL:$1|1=user|2=list of users}}, {{PLURAL:$3|by|by any of}} $2",
"paramvalidator-help-type-user-subtype-name": "user name",
"paramvalidator-help-type-user-subtype-ip": "IP",
"paramvalidator-help-type-user-subtype-cidr": "IP range",
"paramvalidator-help-type-user-subtype-interwiki": "interwiki name (e.g. \"prefix>ExampleName\")",
"paramvalidator-help-type-user-subtype-id": "user ID (e.g. \"#12345\")"
}

View file

@ -4491,5 +4491,12 @@
"mycustomjsredirectprotected": "Error message shown when user tries to edit their own JS page that is a foreign redirect without the 'mycustomjsredirectprotected' right. See also {{msg-mw|mycustomjsprotected}}.",
"deflate-invaliddeflate": "Error message if the content passed to Deflate was not deflated (compressed) properly",
"unprotected-js": "Error message shown when trying to load javascript via action=raw that is not protected",
"userlogout-continue": "Shown if user attempted to log out without a token specified. Probably the user clicked on an old link that hasn't been updated to use the new system. $1 - url that user should click on in order to log out."
"userlogout-continue": "Shown if user attempted to log out without a token specified. Probably the user clicked on an old link that hasn't been updated to use the new system. $1 - url that user should click on in order to log out.",
"paramvalidator-baduser": "Error in API parameter validation. Parameters:\n* $1 - Parameter name.\n* $2 - Parameter value.",
"paramvalidator-help-type-user": "Used to indicate that a parameter is a user or list of users. Parameters:\n* $1 - 1 if the parameter takes one value, 2 if the parameter takes any number of values\n* $2 - List of allowed ways to specify the user, as an 'and'-style list.\n* $3 - Number of items in $2.\n\nSee also:\n* {{msg-mw|paramvalidator-help-type-user-subtype-name}}\n* {{msg-mw|paramvalidator-help-type-user-subtype-ip}}\n* {{msg-mw|paramvalidator-help-type-user-subtype-cidr}}\n* {{msg-mw|paramvalidator-help-type-user-subtype-interwiki}}\n* {{msg-mw|paramvalidator-help-type-user-subtype-id}}",
"paramvalidator-help-type-user-subtype-name": "Used with {{msg-mw|paramvalidator-help-type-user}} to indicate that users may be specified by name.",
"paramvalidator-help-type-user-subtype-ip": "Used with {{msg-mw|paramvalidator-help-type-user}} to indicate that IP (anonymous) users may be specified by IP.",
"paramvalidator-help-type-user-subtype-cidr": "Used with {{msg-mw|paramvalidator-help-type-user}} to indicate that IP (anonymous) users may be specified by a CIDR range.",
"paramvalidator-help-type-user-subtype-interwiki": "Used with {{msg-mw|paramvalidator-help-type-user}} to indicate that \"interwiki\" user names may be specified. Interwiki usernames in MediaWiki generally appear due to imports.",
"paramvalidator-help-type-user-subtype-id": "Used with {{msg-mw|paramvalidator-help-type-user}} to indicate that users may be specified by user ID number, prefixed with a \"#\" character."
}

View file

@ -1771,15 +1771,13 @@ return [
'apisandbox-templated-parameter-reason',
'apisandbox-deprecated-parameters',
'apisandbox-no-parameters',
'api-help-param-limit',
'api-help-param-limit2',
'api-help-param-integer-min',
'api-help-param-integer-max',
'api-help-param-integer-minmax',
'paramvalidator-help-type-number-min',
'paramvalidator-help-type-number-max',
'paramvalidator-help-type-number-minmax',
'api-help-param-multi-separate',
'api-help-param-multi-max',
'api-help-param-maxbytes',
'api-help-param-maxchars',
'paramvalidator-help-multi-max',
'paramvalidator-help-type-string-maxbytes',
'paramvalidator-help-type-string-maxchars',
'apisandbox-submit-invalid-fields-title',
'apisandbox-submit-invalid-fields-message',
'apisandbox-results',

View file

@ -477,9 +477,7 @@
widget.getValidity = widget.input.getValidity.bind( widget.input );
widget.paramInfo = pi;
$.extend( widget, WidgetMethods.textInputWidget );
if ( Util.apiBool( pi.enforcerange ) ) {
widget.setRange( pi.min || -Infinity, pi.max || Infinity );
}
widget.setRange( pi.min || -Infinity, pi.max || Infinity );
multiModeAllowed = true;
multiModeInput = widget;
break;
@ -1437,27 +1435,17 @@
break;
case 'limit':
if ( ppi.highmax !== undefined ) {
$descriptionContainer.append( $( '<div>' )
.addClass( 'info' )
.append(
Util.parseMsg(
'api-help-param-limit2', ppi.max, ppi.highmax
),
' ',
Util.parseMsg( 'apisandbox-param-limit' )
)
);
} else {
$descriptionContainer.append( $( '<div>' )
.addClass( 'info' )
.append(
Util.parseMsg( 'api-help-param-limit', ppi.max ),
' ',
Util.parseMsg( 'apisandbox-param-limit' )
)
);
}
$descriptionContainer.append( $( '<div>' )
.addClass( 'info' )
.append(
Util.parseMsg(
'paramvalidator-help-type-number-minmax', 1,
ppi.min, ppi.highmax !== undefined ? ppi.highmax : ppi.max
),
' ',
Util.parseMsg( 'apisandbox-param-limit' )
)
);
break;
case 'integer':
@ -1472,7 +1460,7 @@
$descriptionContainer.append( $( '<div>' )
.addClass( 'info' )
.append( Util.parseMsg(
'api-help-param-integer-' + tmp,
'paramvalidator-help-type-number-' + tmp,
Util.apiBool( ppi.multi ) ? 2 : 1,
ppi.min, ppi.max
) )
@ -1499,7 +1487,7 @@
}
if ( count > ppi.lowlimit ) {
tmp.push(
mw.message( 'api-help-param-multi-max', ppi.lowlimit, ppi.highlimit ).parse()
mw.message( 'paramvalidator-help-multi-max', ppi.lowlimit, ppi.highlimit ).parse()
);
}
if ( tmp.length ) {
@ -1512,13 +1500,13 @@
if ( 'maxbytes' in ppi ) {
$descriptionContainer.append( $( '<div>' )
.addClass( 'info' )
.append( Util.parseMsg( 'api-help-param-maxbytes', ppi.maxbytes ) )
.append( Util.parseMsg( 'paramvalidator-help-type-string-maxbytes', ppi.maxbytes ) )
);
}
if ( 'maxchars' in ppi ) {
$descriptionContainer.append( $( '<div>' )
.addClass( 'info' )
.append( Util.parseMsg( 'api-help-param-maxchars', ppi.maxchars ) )
.append( Util.parseMsg( 'paramvalidator-help-type-string-maxchars', ppi.maxchars ) )
);
}
if ( ppi.usedTemplateVars && ppi.usedTemplateVars.length ) {

View file

@ -0,0 +1,147 @@
<?php
namespace MediaWiki\ParamValidator\TypeDef;
use ApiResult;
use MediaWiki\MediaWikiServices;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\TypeDef\TypeDefTestCase;
use Wikimedia\ParamValidator\ValidationException;
/**
* @covers MediaWiki\ParamValidator\TypeDef\NamespaceDef
*/
class NamespaceDefTest extends TypeDefTestCase {
protected static $testClass = NamespaceDef::class;
protected function getInstance( SimpleCallbacks $callbacks, array $options ) {
return new static::$testClass(
$callbacks,
MediaWikiServices::getInstance()->getNamespaceInfo()
);
}
private static function getNamespaces( $extra = [] ) {
$namespaces = array_merge(
MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces(),
$extra
);
sort( $namespaces );
return $namespaces;
}
public function provideValidate() {
$settings = [
ParamValidator::PARAM_TYPE => 'namespace',
];
$extraSettings = [
ParamValidator::PARAM_TYPE => 'namespace',
NamespaceDef::PARAM_EXTRA_NAMESPACES => [ -5 ],
];
return [
'Basic' => [ '0', 0, $settings ],
'Bad namespace' => [
'x',
new ValidationException(
DataMessageValue::new( 'paramvalidator-badvalue', [], 'badvalue', [] ), 'test', 'x', $settings
),
$settings
],
'Unknown namespace' => [
'x',
new ValidationException(
DataMessageValue::new( 'paramvalidator-badvalue', [], 'badvalue', [] ), 'test', '-1', []
),
],
'Extra namespaces' => [ '-5', -5, $extraSettings ],
];
}
public function provideGetEnumValues() {
return [
'Basic test' => [
[ ParamValidator::PARAM_TYPE => 'namespace' ],
self::getNamespaces(),
],
'Extra namespaces' => [
[
ParamValidator::PARAM_TYPE => 'namespace',
NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_SPECIAL, NS_MEDIA ]
],
self::getNamespaces( [ NS_SPECIAL, NS_MEDIA ] ),
],
];
}
public function provideNormalizeSettings() {
return [
'Basic test' => [ [], [] ],
'Add PARAM_ALL' => [
[ ParamValidator::PARAM_ISMULTI => true ],
[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => true ],
],
'Force PARAM_ALL' => [
[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => false ],
[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => true ],
],
'Force PARAM_ALL (2)' => [
[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => 'all' ],
[ ParamValidator::PARAM_ISMULTI => true, ParamValidator::PARAM_ALL => true ],
],
];
}
public function provideStringifyValue() {
return [
'Basic test' => [ 123, '123' ],
'Array' => [ [ 1, 2, 3 ], '1|2|3' ],
];
}
public function provideGetInfo() {
yield 'Basic test' => [
[],
[ 'type' => 'namespace' ],
[
// phpcs:ignore Generic.Files.LineLength.TooLong
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>1</text><list listType="comma"><text>0</text><text>1</text><text>2</text><text>3</text><text>4</text><text>5</text><text>6</text><text>7</text><text>8</text><text>9</text><text>10</text><text>11</text><text>12</text><text>13</text><text>14</text><text>15</text></list><num>16</num></message>',
ParamValidator::PARAM_ISMULTI => null,
],
];
yield 'Extra namespaces' => [
[
ParamValidator::PARAM_DEFAULT => 0,
NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_SPECIAL, NS_MEDIA ]
],
[ 'type' => 'namespace', 'extranamespaces' => [ NS_SPECIAL, NS_MEDIA ] ],
[
// phpcs:ignore Generic.Files.LineLength.TooLong
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>1</text><list listType="comma"><text>-1</text><text>-2</text><text>0</text><text>1</text><text>2</text><text>3</text><text>4</text><text>5</text><text>6</text><text>7</text><text>8</text><text>9</text><text>10</text><text>11</text><text>12</text><text>13</text><text>14</text><text>15</text></list><num>18</num></message>',
ParamValidator::PARAM_ISMULTI => null,
],
];
yield 'Extra namespaces, for Action API' => [
[ NamespaceDef::PARAM_EXTRA_NAMESPACES => [ NS_SPECIAL, NS_MEDIA ] ],
[
'type' => 'namespace',
'extranamespaces' => [
NS_SPECIAL, NS_MEDIA,
ApiResult::META_INDEXED_TAG_NAME => 'ns',
],
],
[
// phpcs:ignore Generic.Files.LineLength.TooLong
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>1</text><list listType="comma"><text>-1</text><text>-2</text><text>0</text><text>1</text><text>2</text><text>3</text><text>4</text><text>5</text><text>6</text><text>7</text><text>8</text><text>9</text><text>10</text><text>11</text><text>12</text><text>13</text><text>14</text><text>15</text></list><num>18</num></message>',
ParamValidator::PARAM_ISMULTI => null,
],
[ 'module' => (object)[] ],
];
}
}

View file

@ -0,0 +1,125 @@
<?php
namespace MediaWiki\ParamValidator\TypeDef;
use ChangeTags;
use MediaWikiIntegrationTestCase;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\SimpleCallbacks;
use Wikimedia\ParamValidator\ValidationException;
/**
* @covers MediaWiki\ParamValidator\TypeDef\TagsDef
*/
class TagsDefTest extends MediaWikiIntegrationTestCase {
protected static $testClass = TagsDef::class;
protected function setUp(): void {
parent::setUp();
ChangeTags::defineTag( 'tag1' );
ChangeTags::defineTag( 'tag2' );
$this->tablesUsed[] = 'change_tag_def';
$this->tablesUsed[] = 'valid_tag';
// Since the type def shouldn't care about the specific user,
// remove the right from relevant groups to ensure that it's not
// checking.
$this->setGroupPermissions( [
'*' => [ 'applychangetags' => false ],
'user' => [ 'applychangetags' => false ],
] );
}
/**
* @dataProvider provideValidate
* @param mixed $value Value for getCallbacks()
* @param mixed|ValidationException $expect Expected result from TypeDef::validate().
* If a ValidationException, it is expected that a ValidationException
* with matching failure code and data will be thrown. Otherwise, the return value must be equal.
* @param array $settings Settings array.
* @param array $options Options array
* @param array[] $expectConds Expected conditions reported. Each array is
* `[ $ex->getFailureCode() ] + $ex->getFailureData()`.
*/
public function testValidate(
$value, $expect, array $settings = [], array $options = [], array $expectConds = []
) {
$callbacks = new SimpleCallbacks( [ 'test' => $value ] );
$typeDef = new TagsDef( $callbacks );
$settings = $typeDef->normalizeSettings( $settings );
if ( $expect instanceof ValidationException ) {
try {
$v = $typeDef->getValue( 'test', $settings, $options );
$typeDef->validate( 'test', $v, $settings, $options );
$this->fail( 'Expected exception not thrown' );
} catch ( ValidationException $ex ) {
$this->assertSame(
$expect->getFailureMessage()->getCode(),
$ex->getFailureMessage()->getCode()
);
$this->assertSame(
$expect->getFailureMessage()->getData(),
$ex->getFailureMessage()->getData()
);
}
} else {
$v = $typeDef->getValue( 'test', $settings, $options );
$this->assertEquals( $expect, $typeDef->validate( 'test', $v, $settings, $options ) );
}
$conditions = [];
foreach ( $callbacks->getRecordedConditions() as $c ) {
$conditions[] = [ 'code' => $c['message']->getCode(), 'data' => $c['message']->getData() ];
}
$this->assertSame( $expectConds, $conditions );
}
public function provideValidate() {
$settings = [
ParamValidator::PARAM_TYPE => 'tags',
ParamValidator::PARAM_ISMULTI => true,
];
$valuesList = [ 'values-list' => [ 'tag1', 'doesnotexist', 'doesnotexist2' ] ];
return [
'Basic' => [ 'tag1', [ 'tag1' ] ],
'Bad tag' => [
'doesnotexist',
new ValidationException(
DataMessageValue::new( 'paramvalidator-badtags', [], 'badtags', [
'disallowedtags' => [ 'doesnotexist' ],
] ),
'test', 'doesnotexist', []
),
],
'Multi' => [ 'tag1', 'tag1', $settings, [ 'values-list' => [ 'tag1', 'tag2' ] ] ],
'Multi with bad tag (but not the tag)' => [
'tag1', 'tag1', $settings, $valuesList
],
'Multi with bad tag' => [
'doesnotexist',
new ValidationException(
DataMessageValue::new( 'paramvalidator-badtags', [], 'badtags', [
'disallowedtags' => [ 'doesnotexist', 'doesnotexist2' ],
] ),
'test', 'doesnotexist', $settings
),
$settings, $valuesList
],
];
}
public function testGetEnumValues() {
$typeDef = new TagsDef( new SimpleCallbacks( [] ) );
$this->assertSame(
ChangeTags::listExplicitlyDefinedTags(),
$typeDef->getEnumValues( 'test', [], [] )
);
}
}

View file

@ -0,0 +1,193 @@
<?php
namespace MediaWiki\ParamValidator\TypeDef;
use MediaWiki\User\UserIdentityValue;
use User;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef\TypeDefTestCase;
use Wikimedia\ParamValidator\ValidationException;
/**
* @covers MediaWiki\ParamValidator\TypeDef\UserDef
*/
class UserDefTest extends TypeDefTestCase {
protected static $testClass = UserDef::class;
private $wgHooks = null;
protected function setUp(): void {
global $wgHooks;
parent::setUp();
// We don't have MediaWikiIntegrationTestCase's methods available, so we have to do it ourself.
$this->wgHooks = $wgHooks;
$wgHooks['InterwikiLoadPrefix'][] = function ( $prefix, &$iwdata ) {
if ( $prefix === 'interwiki' ) {
$iwdata = [
'iw_url' => 'http://example.com/',
'iw_local' => 0,
'iw_trans' => 0,
];
return false;
}
};
}
protected function tearDown(): void {
global $wgHooks;
$wgHooks = $this->wgHooks;
parent::tearDown();
}
public function provideValidate() {
// General tests of string inputs
$data = [
'Basic' => [ 'name', 'Some user', 'Some user' ],
'Normalized' => [ 'name', 'some_user', 'Some user' ],
'External' => [ 'interwiki', 'm>some_user', 'm>some_user' ],
'IPv4' => [ 'ip', '192.168.0.1', '192.168.0.1' ],
'IPv4, normalized' => [ 'ip', '192.168.000.001', '192.168.0.1' ],
'IPv6' => [ 'ip', '2001:DB8:0:0:0:0:0:0', '2001:DB8:0:0:0:0:0:0' ],
'IPv6, normalized' => [ 'ip', '2001:0db8::', '2001:DB8:0:0:0:0:0:0' ],
'IPv6, with leading ::' => [ 'ip', '::1', '0:0:0:0:0:0:0:1' ],
'IPv4 range' => [ 'cidr', '192.168.000.000/16', '192.168.0.0/16' ],
'IPv6 range' => [ 'cidr', '2001:0DB8::/64', '2001:DB8:0:0:0:0:0:0/64' ],
'Usemod IP' => [ 'ip', '192.168.0.xxx', '192.168.0.xxx' ],
'Bogus IP' => [ '', '192.168.0.256', null ],
'Bogus Usemod IP' => [ '', '192.268.0.xxx', null ],
'Usemod IP as range' => [ '', '192.168.0.xxx/16', null ],
'Bad username' => [ '', '[[Foo]]', null ],
'No namespaces' => [ '', 'Talk:Foo', null ],
'No namespaces (2)' => [ '', 'Help:Foo', null ],
'No namespaces (except User is ok)' => [ 'name', 'User:some_user', 'Some user' ],
'No namespaces (except User is ok) (IPv6)' => [ 'ip', 'User:::1', '0:0:0:0:0:0:0:1' ],
'No interwiki prefixes' => [ '', 'interwiki:Foo', null ],
'No fragment in IP' => [ '', '192.168.0.256#', null ],
];
foreach ( $data as $key => [ $type, $input, $expect ] ) {
$ex = new ValidationException(
DataMessageValue::new( 'paramvalidator-baduser', [], 'baduser' ),
'test', $input, []
);
if ( $type === '' ) {
yield $key => [ $input, $ex ];
continue;
}
yield $key => [ $input, $expect ];
yield "$key, only '$type' allowed" => [
$input,
$expect,
[ UserDef::PARAM_ALLOWED_USER_TYPES => [ $type ] ],
];
$types = array_diff( [ 'name', 'ip', 'cidr', 'interwiki' ], [ $type ] );
yield "$key, without '$type' allowed" => [
$input,
$ex,
[ UserDef::PARAM_ALLOWED_USER_TYPES => $types ],
];
$obj = $type === 'name' ? User::newFromName( $expect ) : new UserIdentityValue( 0, $expect, 0 );
yield "$key, returning object" => [ $input, $obj, [ UserDef::PARAM_RETURN_OBJECT => true ] ];
}
// Test input by user ID
// We can't test not returning object here, because we don't have a test
// database and there's no "UserFactory" (yet) to inject a mock of.
$input = '#1234';
$ex = new ValidationException(
DataMessageValue::new( 'paramvalidator-baduser', [], 'baduser' ),
'test', $input, []
);
yield 'User ID' => [ $input, $ex, [ UserDef::PARAM_RETURN_OBJECT => true ] ];
yield 'User ID, with \'id\' allowed, returning object' => [
$input,
User::newFromId( 1234 ),
[ UserDef::PARAM_ALLOWED_USER_TYPES => [ 'id' ], UserDef::PARAM_RETURN_OBJECT => true ],
];
// Tests for T232672 (consistent treatment of whitespace and BIDI characters)
$data = [
'name' => [ 'Foo', [ 1 ], 'Foo' ],
'interwiki' => [ 'm>some_user', [ 1, 2, 6 ], null ],
'ip (v4)' => [ '192.168.0.1', [ 1, 3, 4 ], '192.168.0.1' ],
'ip (v6)' => [ '2001:DB8:0:0:0:0:0:0', [ 2, 5, 6 ], '2001:DB8:0:0:0:0:0:0' ],
'ip (v6, colons)' => [ '::1', [ 1, 2 ], '0:0:0:0:0:0:0:1' ],
'cidr (v4)' => [ '192.168.0.0/16', [ 1, 3, 4, 11, 12, 13 ], '192.168.0.0/16' ],
'cidr (v6)' => [ '2001:db8::/64', [ 2, 5, 6, 20, 21, 22 ], '2001:DB8:0:0:0:0:0:0/64' ],
];
foreach ( $data as $key => [ $name, $positions, $expect ] ) {
$input = " $name ";
yield "T232672: leading/trailing whitespace for $key" => [ $input, $expect ?? $input ];
$input = "_{$name}_";
yield "T232672: leading/trailing underscores for $key" => [ $input, $expect ?? $input ];
$positions = array_merge( [ 0, strlen( $name ) ], $positions );
foreach ( $positions as $i ) {
$input = substr_replace( $name, "\u{200E}", $i, 0 );
yield "T232672: U+200E at position $i for $key" => [ $input, $expect ?? $input ];
}
}
}
public function provideNormalizeSettings() {
return [
'Basic test' => [
[ 'param-foo' => 'bar' ],
[
'param-foo' => 'bar',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'cidr', 'interwiki' ],
],
],
'Types not overridden' => [
[
'param-foo' => 'bar',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
],
[
'param-foo' => 'bar',
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
],
],
];
}
public function provideGetInfo() {
return [
'Basic test' => [
[],
[
'subtypes' => [ 'name', 'ip', 'cidr', 'interwiki' ],
],
[
// phpcs:ignore Generic.Files.LineLength.TooLong
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-user"><text>1</text><list listType="text"><text><message key="paramvalidator-help-type-user-subtype-name"></message></text><text><message key="paramvalidator-help-type-user-subtype-ip"></message></text><text><message key="paramvalidator-help-type-user-subtype-cidr"></message></text><text><message key="paramvalidator-help-type-user-subtype-interwiki"></message></text></list><num>4</num></message>',
],
],
'Specific types' => [
[
ParamValidator::PARAM_ISMULTI => true,
UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'id' ],
UserDef::PARAM_RETURN_OBJECT => true,
],
[
'subtypes' => [ 'name', 'id' ],
],
[
// phpcs:ignore Generic.Files.LineLength.TooLong
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-user"><text>2</text><list listType="text"><text><message key="paramvalidator-help-type-user-subtype-name"></message></text><text><message key="paramvalidator-help-type-user-subtype-id"></message></text></list><num>2</num></message>',
],
],
];
}
}

View file

@ -244,7 +244,7 @@ class ApiBaseTest extends ApiTestCase {
$wrapper->getParameter( 'foo' );
$this->fail( 'Expected exception not thrown' );
} catch ( ApiUsageException $ex ) {
$this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
$this->assertTrue( $this->apiExceptionHasCode( $ex, 'badvalue' ) );
}
// And extractRequestParams() must throw too.
@ -252,7 +252,7 @@ class ApiBaseTest extends ApiTestCase {
$mock->extractRequestParams();
$this->fail( 'Expected exception not thrown' );
} catch ( ApiUsageException $ex ) {
$this->assertTrue( $this->apiExceptionHasCode( $ex, 'unknown_foo' ) );
$this->assertTrue( $this->apiExceptionHasCode( $ex, 'badvalue' ) );
}
}
@ -263,7 +263,6 @@ class ApiBaseTest extends ApiTestCase {
* @param array $options Key-value pairs:
* 'parseLimits': true|false
* 'apihighlimits': true|false
* 'internalmode': true|false
* 'prefix': true|false
* @param string[] $warnings
*/
@ -290,11 +289,6 @@ class ApiBaseTest extends ApiTestCase {
$context->setUser( self::$users['sysop']->getUser() );
}
if ( isset( $options['internalmode'] ) && !$options['internalmode'] ) {
$mainWrapper = TestingAccessWrapper::newFromObject( $wrapper->mMainModule );
$mainWrapper->mInternalMode = false;
}
// If we're testing tags, set up some tags
if ( isset( $paramSettings[ApiBase::PARAM_TYPE] ) &&
$paramSettings[ApiBase::PARAM_TYPE] === 'tags'
@ -309,7 +303,14 @@ class ApiBaseTest extends ApiTestCase {
$parseLimits );
$this->fail( 'No exception thrown' );
} catch ( Exception $ex ) {
$this->assertEquals( $expected, $ex );
$this->assertInstanceOf( get_class( $expected ), $ex );
if ( $ex instanceof ApiUsageException ) {
$this->assertEquals( $expected->getModulePath(), $ex->getModulePath() );
$this->assertEquals( $expected->getStatusValue(), $ex->getStatusValue() );
} else {
$this->assertEquals( $expected->getMessage(), $ex->getMessage() );
$this->assertEquals( $expected->getCode(), $ex->getCode() );
}
}
} else {
$result = $wrapper->getParameterFromSettings( $paramName,
@ -382,6 +383,8 @@ class ApiBaseTest extends ApiTestCase {
: '<27>';
}
$namespaces = MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces();
$returnArray = [
'Basic param' => [ 'bar', null, 'bar', [] ],
'Basic param, C0 controls' => [ $c0, null, $enc, $warnings ],
@ -391,8 +394,11 @@ class ApiBaseTest extends ApiTestCase {
'String param, required, empty' => [
'',
[ ApiBase::PARAM_DFLT => 'default', ApiBase::PARAM_REQUIRED => true ],
ApiUsageException::newWithMessage( null,
[ 'apierror-missingparam', 'myParam' ] ),
ApiUsageException::newWithMessage( null, [
'paramvalidator-missingparam',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '' ),
], 'missingparam' ),
[]
],
'Multi-valued parameter' => [
@ -434,9 +440,15 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_ISMULTI_LIMIT1 => 2,
],
ApiUsageException::newWithMessage(
null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
),
ApiUsageException::newWithMessage( null, [
'paramvalidator-toomanyvalues',
Message::plaintextParam( 'myParam' ),
Message::numParam( 2 ),
], 'toomanyvalues', [
'limit' => 2,
'lowlimit' => 2,
'highlimit' => 500,
] ),
[]
],
'Multi-valued parameter with exceeded limits for non-bot' => [
@ -446,9 +458,15 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_ISMULTI_LIMIT1 => 2,
ApiBase::PARAM_ISMULTI_LIMIT2 => 3,
],
ApiUsageException::newWithMessage(
null, [ 'apierror-toomanyvalues', 'myParam', 2 ], 'too-many-myParam'
),
ApiUsageException::newWithMessage( null, [
'paramvalidator-toomanyvalues',
Message::plaintextParam( 'myParam' ),
Message::numParam( 2 ),
], 'toomanyvalues', [
'limit' => 2,
'lowlimit' => 2,
'highlimit' => 3,
] ),
[]
],
'Multi-valued parameter with non-exceeded limits for bot' => [
@ -465,9 +483,7 @@ class ApiBaseTest extends ApiTestCase {
'Multi-valued parameter with prohibited duplicates' => [
'a|b|a|c',
[ ApiBase::PARAM_ISMULTI => true ],
// Note that the keys are not sequential! This matches
// array_unique, but might be unexpected.
[ 0 => 'a', 1 => 'b', 3 => 'c' ],
[ 'a', 'b', 'c' ],
[],
],
'Multi-valued parameter with allowed duplicates' => [
@ -497,35 +513,15 @@ class ApiBaseTest extends ApiTestCase {
true,
[],
],
'Boolean multi-param' => [
'true|false',
[
ApiBase::PARAM_TYPE => 'boolean',
ApiBase::PARAM_ISMULTI => true,
],
new MWException(
'Internal error in ApiBase::getParameterFromSettings: ' .
'Multi-values not supported for myParam'
),
[],
],
'Empty boolean param with non-false default' => [
'',
[
ApiBase::PARAM_TYPE => 'boolean',
ApiBase::PARAM_DFLT => true,
],
new MWException(
'Internal error in ApiBase::getParameterFromSettings: ' .
"Boolean param myParam's default is set to '1'. " .
'Boolean parameters must default to false.' ),
[],
],
'Deprecated parameter' => [
'foo',
[ ApiBase::PARAM_DEPRECATED => true ],
'foo',
[ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
[ [
'paramvalidator-param-deprecated',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( 'foo' )
] ],
],
'Deprecated parameter with default, unspecified' => [
null,
@ -537,50 +533,87 @@ class ApiBaseTest extends ApiTestCase {
'foo',
[ ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_DFLT => 'foo' ],
'foo',
[ [ 'apiwarn-deprecation-parameter', 'myParam' ] ],
[ [
'paramvalidator-param-deprecated',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( 'foo' )
] ],
],
'Deprecated parameter value' => [
'a',
[ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ] ],
[ ApiBase::PARAM_TYPE => [ 'a' ], ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ] ],
'a',
[ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
[ [
'paramvalidator-deprecated-value',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( 'a' )
] ],
],
'Deprecated parameter value as default, unspecified' => [
null,
[ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ], ApiBase::PARAM_DFLT => 'a' ],
[
ApiBase::PARAM_TYPE => [ 'a' ],
ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ],
ApiBase::PARAM_DFLT => 'a'
],
'a',
[],
],
'Deprecated parameter value as default, specified' => [
'a',
[ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ], ApiBase::PARAM_DFLT => 'a' ],
[
ApiBase::PARAM_TYPE => [ 'a' ],
ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => true ],
ApiBase::PARAM_DFLT => 'a'
],
'a',
[ [ 'apiwarn-deprecation-parameter', 'myParam=a' ] ],
[ [
'paramvalidator-deprecated-value',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( 'a' )
] ],
],
'Multiple deprecated parameter values' => [
'a|b|c|d',
[ ApiBase::PARAM_DEPRECATED_VALUES =>
[ 'b' => true, 'd' => true ],
ApiBase::PARAM_ISMULTI => true ],
[
ApiBase::PARAM_TYPE => [ 'a', 'b', 'c', 'd' ],
ApiBase::PARAM_DEPRECATED_VALUES => [ 'b' => true, 'd' => true ],
ApiBase::PARAM_ISMULTI => true,
],
[ 'a', 'b', 'c', 'd' ],
[
[ 'apiwarn-deprecation-parameter', 'myParam=b' ],
[ 'apiwarn-deprecation-parameter', 'myParam=d' ],
[
'paramvalidator-deprecated-value',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( 'b' )
],
[
'paramvalidator-deprecated-value',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( 'd' )
],
],
],
'Deprecated parameter value with custom warning' => [
'a',
[ ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => 'my-msg' ] ],
[ ApiBase::PARAM_TYPE => [ 'a' ], ApiBase::PARAM_DEPRECATED_VALUES => [ 'a' => 'my-msg' ] ],
'a',
[ 'my-msg' ],
[ [ 'my-msg' ] ],
],
'"*" when wildcard not allowed' => [
'*',
[ ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ] ],
[
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
],
[],
[ [ 'apiwarn-unrecognizedvalues', 'myParam',
[ 'list' => [ '&#42;' ], 'type' => 'comma' ], 1 ] ],
[ [
'paramvalidator-unrecognizedvalues',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '*' ),
Message::listParam( [ Message::plaintextParam( '*' ) ], 'comma' ),
Message::numParam( 1 ),
] ],
],
'Wildcard "*"' => [
'*',
@ -598,9 +631,17 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
ApiBase::PARAM_ALL => true,
],
ApiUsageException::newWithMessage( null,
[ 'apierror-unrecognizedvalue', 'myParam', '&#42;' ],
'unknown_myParam' ),
ApiUsageException::newWithMessage( null, [
'paramvalidator-badvalue-enumnotmulti',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '*' ),
Message::listParam( [
Message::plaintextParam( 'a' ),
Message::plaintextParam( 'b' ),
Message::plaintextParam( 'c' ),
] ),
Message::numParam( 3 ),
], 'badvalue' ),
[],
],
'Wildcard "*" with unrestricted type' => [
@ -622,26 +663,13 @@ class ApiBaseTest extends ApiTestCase {
[ 'a', 'b', 'c' ],
[],
],
'Wildcard conflicting with allowed value' => [
'a',
[
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => [ 'a', 'b', 'c' ],
ApiBase::PARAM_ALL => 'a',
],
new MWException(
'Internal error in ApiBase::getParameterFromSettings: ' .
'For param myParam, PARAM_ALL collides with a possible ' .
'value' ),
[],
],
'Namespace with wildcard' => [
'*',
[
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => 'namespace',
],
MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces(),
$namespaces,
[],
],
// PARAM_ALL is ignored with namespace types.
@ -652,7 +680,7 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_TYPE => 'namespace',
ApiBase::PARAM_ALL => false,
],
MediaWikiServices::getInstance()->getNamespaceInfo()->getValidNamespaces(),
$namespaces,
[],
],
'Namespace with wildcard "x"' => [
@ -663,13 +691,18 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_ALL => 'x',
],
[],
[ [ 'apiwarn-unrecognizedvalues', 'myParam',
[ 'list' => [ 'x' ], 'type' => 'comma' ], 1 ] ],
[ [
'paramvalidator-unrecognizedvalues',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( 'x' ),
Message::listParam( [ Message::plaintextParam( 'x' ) ], 'comma' ),
Message::numParam( 1 ),
] ],
],
'Password' => [
'dDy+G?e?txnr.1:(@[Ru',
'dDy+G?e?txnr.1:(@Ru',
[ ApiBase::PARAM_TYPE => 'password' ],
'dDy+G?e?txnr.1:(@[Ru',
'dDy+G?e?txnr.1:(@Ru',
[],
],
'Sensitive field' => [
@ -678,45 +711,26 @@ class ApiBaseTest extends ApiTestCase {
'I am fond of pineapples',
[],
],
'Upload with default' => [
'',
[
ApiBase::PARAM_TYPE => 'upload',
ApiBase::PARAM_DFLT => '',
],
new MWException(
'Internal error in ApiBase::getParameterFromSettings: ' .
"File upload param myParam's default is set to ''. " .
'File upload parameters may not have a default.' ),
[],
],
'Multiple upload' => [
'',
[
ApiBase::PARAM_TYPE => 'upload',
ApiBase::PARAM_ISMULTI => true,
],
new MWException(
'Internal error in ApiBase::getParameterFromSettings: ' .
'Multi-values not supported for myParam' ),
[],
],
// @todo Test actual upload
'Namespace -1' => [
'-1',
[ ApiBase::PARAM_TYPE => 'namespace' ],
ApiUsageException::newWithMessage( null,
[ 'apierror-unrecognizedvalue', 'myParam', '-1' ],
'unknown_myParam' ),
ApiUsageException::newWithMessage( null, [
'paramvalidator-badvalue-enumnotmulti',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '-1' ),
Message::listParam( array_map( 'Message::plaintextParam', $namespaces ) ),
Message::numParam( count( $namespaces ) ),
], 'badvalue' ),
[],
],
'Extra namespace -1' => [
'-1',
[
ApiBase::PARAM_TYPE => 'namespace',
ApiBase::PARAM_EXTRA_NAMESPACES => [ '-1' ],
ApiBase::PARAM_EXTRA_NAMESPACES => [ -1 ],
],
'-1',
-1,
[],
],
// @todo Test with PARAM_SUBMODULE_MAP unset, need
@ -728,23 +742,26 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_SUBMODULE_MAP =>
[ 'foo' => 'foo', 'bar' => 'foo+bar' ],
],
ApiUsageException::newWithMessage(
null,
[
'apierror-unrecognizedvalue',
'myParam',
'not-a-module-name',
],
'unknown_myParam'
),
ApiUsageException::newWithMessage( null, [
'paramvalidator-badvalue-enumnotmulti',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( 'not-a-module-name' ),
Message::listParam( [
Message::plaintextParam( 'foo' ),
Message::plaintextParam( 'bar' ),
] ),
Message::numParam( 2 ),
], 'badvalue' ),
[],
],
'\\x1f with multiples not allowed' => [
"\x1f",
[],
ApiUsageException::newWithMessage( null,
'apierror-badvalue-notmultivalue',
'badvalue_notmultivalue' ),
ApiUsageException::newWithMessage( null, [
'paramvalidator-notmulti',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( "\x1f" ),
], 'badvalue' ),
[],
],
'Integer with unenforced min' => [
@ -754,8 +771,13 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_MIN => -1,
],
-1,
[ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
-2 ] ],
[ [
'paramvalidator-outofrange-min',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '-2' ),
Message::numParam( -1 ),
Message::numParam( '' ),
] ],
],
'Integer with enforced min' => [
'-2',
@ -764,56 +786,45 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_MIN => -1,
ApiBase::PARAM_RANGE_ENFORCE => true,
],
ApiUsageException::newWithMessage( null,
[ 'apierror-integeroutofrange-belowminimum', 'myParam',
'-1', '-2' ], 'integeroutofrange',
[ 'min' => -1, 'max' => null, 'botMax' => null ] ),
ApiUsageException::newWithMessage( null, [
'paramvalidator-outofrange-min',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '-2' ),
Message::numParam( -1 ),
Message::numParam( '' ),
], 'outofrange', [ 'min' => -1, 'curmax' => null, 'max' => null, 'highmax' => null ] ),
[],
],
'Integer with unenforced max (internal mode)' => [
'8',
[
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_MAX => 7,
],
8,
[],
],
'Integer with enforced max (internal mode)' => [
'8',
[
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_MAX => 7,
ApiBase::PARAM_RANGE_ENFORCE => true,
],
8,
[],
],
'Integer with unenforced max (non-internal mode)' => [
'Integer with unenforced max' => [
'8',
[
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_MAX => 7,
],
7,
[ [ 'apierror-integeroutofrange-abovemax', 'myParam', 7, 8 ] ],
[ 'internalmode' => false ],
[ [
'paramvalidator-outofrange-max',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '8' ),
Message::numParam( '' ),
Message::numParam( 7 ),
] ],
],
'Integer with enforced max (non-internal mode)' => [
'Integer with enforced max' => [
'8',
[
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_MAX => 7,
ApiBase::PARAM_RANGE_ENFORCE => true,
],
ApiUsageException::newWithMessage(
null,
[ 'apierror-integeroutofrange-abovemax', 'myParam', '7', '8' ],
'integeroutofrange',
[ 'min' => null, 'max' => 7, 'botMax' => 7 ]
),
ApiUsageException::newWithMessage( null, [
'paramvalidator-outofrange-max',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '8' ),
Message::numParam( '' ),
Message::numParam( 7 ),
], 'outofrange', [ 'min' => null, 'curmax' => 7, 'max' => 7, 'highmax' => 7 ] ),
[],
[ 'internalmode' => false ],
],
'Array of integers' => [
'3|12|966|-1',
@ -824,35 +835,7 @@ class ApiBaseTest extends ApiTestCase {
[ 3, 12, 966, -1 ],
[],
],
'Array of integers with unenforced min/max (internal mode)' => [
'3|12|966|-1',
[
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_MIN => 0,
ApiBase::PARAM_MAX => 100,
],
[ 3, 12, 966, 0 ],
[ [ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ] ],
],
'Array of integers with enforced min/max (internal mode)' => [
'3|12|966|-1',
[
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_MIN => 0,
ApiBase::PARAM_MAX => 100,
ApiBase::PARAM_RANGE_ENFORCE => true,
],
ApiUsageException::newWithMessage(
null,
[ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ],
'integeroutofrange',
[ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
),
[],
],
'Array of integers with unenforced min/max (non-internal mode)' => [
'Array of integers with unenforced min/max' => [
'3|12|966|-1',
[
ApiBase::PARAM_ISMULTI => true,
@ -862,12 +845,23 @@ class ApiBaseTest extends ApiTestCase {
],
[ 3, 12, 100, 0 ],
[
[ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
[ 'apierror-integeroutofrange-belowminimum', 'myParam', 0, -1 ]
[
'paramvalidator-outofrange-minmax',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '966' ),
Message::numParam( 0 ),
Message::numParam( 100 ),
],
[
'paramvalidator-outofrange-minmax',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '-1' ),
Message::numParam( 0 ),
Message::numParam( 100 ),
],
],
[ 'internalmode' => false ],
],
'Array of integers with enforced min/max (non-internal mode)' => [
'Array of integers with enforced min/max' => [
'3|12|966|-1',
[
ApiBase::PARAM_ISMULTI => true,
@ -876,14 +870,14 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_MAX => 100,
ApiBase::PARAM_RANGE_ENFORCE => true,
],
ApiUsageException::newWithMessage(
null,
[ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 966 ],
'integeroutofrange',
[ 'min' => 0, 'max' => 100, 'botMax' => 100 ]
),
ApiUsageException::newWithMessage( null, [
'paramvalidator-outofrange-minmax',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '966' ),
Message::numParam( 0 ),
Message::numParam( 100 ),
], 'outofrange', [ 'min' => 0, 'curmax' => 100, 'max' => 100, 'highmax' => 100 ] ),
[],
[ 'internalmode' => false ],
],
'Limit with parseLimits false (numeric)' => [
'100',
@ -902,43 +896,20 @@ class ApiBaseTest extends ApiTestCase {
'Limit with parseLimits false (invalid)' => [
'kitten',
[ ApiBase::PARAM_TYPE => 'limit' ],
0,
ApiUsageException::newWithMessage( null, [
'paramvalidator-badinteger',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( 'kitten' ),
], 'badinteger' ),
[],
[ 'parseLimits' => false ],
],
'Limit with no max' => [
'100',
'Limit with no max, supplied "max"' => [
'max',
[
ApiBase::PARAM_TYPE => 'limit',
ApiBase::PARAM_MAX2 => 10,
],
new MWException(
'Internal error in ApiBase::getParameterFromSettings: ' .
'MAX1 or MAX2 are not defined for the limit myParam' ),
[],
],
'Limit with no max2' => [
'100',
[
ApiBase::PARAM_TYPE => 'limit',
ApiBase::PARAM_MAX => 10,
],
new MWException(
'Internal error in ApiBase::getParameterFromSettings: ' .
'MAX1 or MAX2 are not defined for the limit myParam' ),
[],
],
'Limit with multi-value' => [
'100',
[
ApiBase::PARAM_TYPE => 'limit',
ApiBase::PARAM_MAX => 10,
ApiBase::PARAM_MAX2 => 10,
ApiBase::PARAM_ISMULTI => true,
],
new MWException(
'Internal error in ApiBase::getParameterFromSettings: ' .
'Multi-values not supported for myParam' ),
PHP_INT_MAX,
[],
],
'Valid limit' => [
@ -972,39 +943,7 @@ class ApiBaseTest extends ApiTestCase {
[],
[ 'apihighlimits' => true ],
],
'Limit too large (internal mode)' => [
'101',
[
ApiBase::PARAM_TYPE => 'limit',
ApiBase::PARAM_MAX => 100,
ApiBase::PARAM_MAX2 => 101,
],
101,
[],
],
'Limit okay for apihighlimits (internal mode)' => [
'101',
[
ApiBase::PARAM_TYPE => 'limit',
ApiBase::PARAM_MAX => 100,
ApiBase::PARAM_MAX2 => 101,
],
101,
[],
[ 'apihighlimits' => true ],
],
'Limit too large for apihighlimits (internal mode)' => [
'102',
[
ApiBase::PARAM_TYPE => 'limit',
ApiBase::PARAM_MAX => 100,
ApiBase::PARAM_MAX2 => 101,
],
102,
[],
[ 'apihighlimits' => true ],
],
'Limit too large (non-internal mode)' => [
'Limit too large' => [
'101',
[
ApiBase::PARAM_TYPE => 'limit',
@ -1012,10 +951,15 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_MAX2 => 101,
],
100,
[ [ 'apierror-integeroutofrange-abovemax', 'myParam', 100, 101 ] ],
[ 'internalmode' => false ],
[ [
'paramvalidator-outofrange-minmax',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '101' ),
Message::numParam( 0 ),
Message::numParam( 100 ),
] ],
],
'Limit okay for apihighlimits (non-internal mode)' => [
'Limit okay for apihighlimits' => [
'101',
[
ApiBase::PARAM_TYPE => 'limit',
@ -1024,7 +968,7 @@ class ApiBaseTest extends ApiTestCase {
],
101,
[],
[ 'internalmode' => false, 'apihighlimits' => true ],
[ 'apihighlimits' => true ],
],
'Limit too large for apihighlimits (non-internal mode)' => [
'102',
@ -1034,8 +978,14 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_MAX2 => 101,
],
101,
[ [ 'apierror-integeroutofrange-abovebotmax', 'myParam', 101, 102 ] ],
[ 'internalmode' => false, 'apihighlimits' => true ],
[ [
'paramvalidator-outofrange-minmax',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '102' ),
Message::numParam( 0 ),
Message::numParam( 101 ),
] ],
[ 'apihighlimits' => true ],
],
'Limit too small' => [
'-2',
@ -1046,8 +996,13 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_MAX2 => 100,
],
-1,
[ [ 'apierror-integeroutofrange-belowminimum', 'myParam', -1,
-2 ] ],
[ [
'paramvalidator-outofrange-minmax',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '-2' ),
Message::numParam( -1 ),
Message::numParam( 100 ),
] ],
],
'Timestamp' => [
wfTimestamp( TS_UNIX, '20211221122112' ),
@ -1060,13 +1015,21 @@ class ApiBaseTest extends ApiTestCase {
[ ApiBase::PARAM_TYPE => 'timestamp' ],
// Magic keyword
'now',
[ [ 'apiwarn-unclearnowtimestamp', 'myParam', '0' ] ],
[ [
'paramvalidator-unclearnowtimestamp',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '0' ),
] ],
],
'Timestamp empty' => [
'',
[ ApiBase::PARAM_TYPE => 'timestamp' ],
'now',
[ [ 'apiwarn-unclearnowtimestamp', 'myParam', '' ] ],
[ [
'paramvalidator-unclearnowtimestamp',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '' ),
] ],
],
// wfTimestamp() interprets this as Unix time
'Timestamp 00' => [
@ -1084,11 +1047,11 @@ class ApiBaseTest extends ApiTestCase {
'Invalid timestamp' => [
'a potato',
[ ApiBase::PARAM_TYPE => 'timestamp' ],
ApiUsageException::newWithMessage(
null,
[ 'apierror-badtimestamp', 'myParam', 'a potato' ],
'badtimestamp_myParam'
),
ApiUsageException::newWithMessage( null, [
'paramvalidator-badtimestamp',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( 'a potato' ),
], 'badtimestamp' ),
[],
],
'Timestamp array' => [
@ -1115,17 +1078,21 @@ class ApiBaseTest extends ApiTestCase {
'Invalid username "|"' => [
'|',
[ ApiBase::PARAM_TYPE => 'user' ],
ApiUsageException::newWithMessage( null,
[ 'apierror-baduser', 'myParam', '&#124;' ],
'baduser_myParam' ),
ApiUsageException::newWithMessage( null, [
'paramvalidator-baduser',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '|' ),
], 'baduser' ),
[],
],
'Invalid username "300.300.300.300"' => [
'300.300.300.300',
[ ApiBase::PARAM_TYPE => 'user' ],
ApiUsageException::newWithMessage( null,
[ 'apierror-baduser', 'myParam', '300.300.300.300' ],
'baduser_myParam' ),
ApiUsageException::newWithMessage( null, [
'paramvalidator-baduser',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '300.300.300.300' ),
], 'baduser' ),
[],
],
'IP range as username' => [
@ -1149,11 +1116,11 @@ class ApiBaseTest extends ApiTestCase {
'Invalid username containing IP address' => [
'This is [not] valid 1.2.3.xxx, ha!',
[ ApiBase::PARAM_TYPE => 'user' ],
ApiUsageException::newWithMessage(
null,
[ 'apierror-baduser', 'myParam', 'This is &#91;not&#93; valid 1.2.3.xxx, ha!' ],
'baduser_myParam'
),
ApiUsageException::newWithMessage( null, [
'paramvalidator-baduser',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( 'This is [not] valid 1.2.3.xxx, ha!' ),
], 'baduser' ),
[],
],
'External username' => [
@ -1198,17 +1165,18 @@ class ApiBaseTest extends ApiTestCase {
'Invalid tag' => [
'invalid tag',
[ ApiBase::PARAM_TYPE => 'tags' ],
new ApiUsageException( null,
Status::newFatal( 'tags-apply-not-allowed-one',
'invalid tag', 1 ) ),
ApiUsageException::newWithMessage(
null,
[ 'tags-apply-not-allowed-one', 'invalid tag', 1 ],
'badtags',
[ 'disallowedtags' => [ 'invalid tag' ] ]
),
[],
],
'Unrecognized type' => [
'foo',
[ ApiBase::PARAM_TYPE => 'nonexistenttype' ],
new MWException(
'Internal error in ApiBase::getParameterFromSettings: ' .
"Param myParam's type is unknown - nonexistenttype" ),
new DomainException( "Param myParam's type is unknown - nonexistenttype" ),
[],
],
'Too many bytes' => [
@ -1217,8 +1185,13 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_MAX_BYTES => 0,
ApiBase::PARAM_MAX_CHARS => 0,
],
ApiUsageException::newWithMessage( null,
[ 'apierror-maxbytes', 'myParam', 0 ] ),
ApiUsageException::newWithMessage( null, [
'paramvalidator-maxbytes',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '1' ),
Message::numParam( 0 ),
Message::numParam( 1 ),
], 'maxbytes', [ 'maxbytes' => 0, 'maxchars' => 0 ] ),
[],
],
'Too many chars' => [
@ -1227,15 +1200,22 @@ class ApiBaseTest extends ApiTestCase {
ApiBase::PARAM_MAX_BYTES => 4,
ApiBase::PARAM_MAX_CHARS => 1,
],
ApiUsageException::newWithMessage( null,
[ 'apierror-maxchars', 'myParam', 1 ] ),
ApiUsageException::newWithMessage( null, [
'paramvalidator-maxchars',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( '§§' ),
Message::numParam( 1 ),
Message::numParam( 2 ),
], 'maxchars', [ 'maxbytes' => 4, 'maxchars' => 1 ] ),
[],
],
'Omitted required param' => [
null,
[ ApiBase::PARAM_REQUIRED => true ],
ApiUsageException::newWithMessage( null,
[ 'apierror-missingparam', 'myParam' ] ),
ApiUsageException::newWithMessage( null, [
'paramvalidator-missingparam',
Message::plaintextParam( 'myParam' )
], 'missingparam' ),
[],
],
'Empty multi-value' => [
@ -1259,34 +1239,31 @@ class ApiBaseTest extends ApiTestCase {
'Prohibited multi-value' => [
'a|b',
[ ApiBase::PARAM_TYPE => [ 'a', 'b' ] ],
ApiUsageException::newWithMessage( null,
[
'apierror-multival-only-one-of',
'myParam',
Message::listParam( [ '<kbd>a</kbd>', '<kbd>b</kbd>' ] ),
2
],
'multival_myParam'
),
ApiUsageException::newWithMessage( null, [
'paramvalidator-badvalue-enumnotmulti',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( 'a|b' ),
Message::listParam( [ Message::plaintextParam( 'a' ), Message::plaintextParam( 'b' ) ] ),
Message::numParam( 2 ),
], 'badvalue' ),
[],
],
];
// The following really just test PHP's string-to-int conversion.
$integerTests = [
[ '+1', 1 ],
[ '-1', -1 ],
[ '1.5', 1 ],
[ '-1.5', -1 ],
[ '1abc', 1 ],
[ ' 1', 1 ],
[ "\t1", 1, '\t1' ],
[ "\r1", 1, '\r1' ],
[ "\f1", 0, '\f1', 'badutf-8' ],
[ "\n1", 1, '\n1' ],
[ "\v1", 0, '\v1', 'badutf-8' ],
[ "\e1", 0, '\e1', 'badutf-8' ],
[ "\x001", 0, '\x001', 'badutf-8' ],
[ '1.5', null ],
[ '-1.5', null ],
[ '1abc', null ],
[ ' 1', null ],
[ "\t1", null, '\t1' ],
[ "\r1", null, '\r1' ],
[ "\f1", null, '\f1', 'badutf-8' ],
[ "\n1", null, '\n1' ],
[ "\v1", null, '\v1', 'badutf-8' ],
[ "\e1", null, '\e1', 'badutf-8' ],
[ "\x001", null, '\x001', 'badutf-8' ],
];
foreach ( $integerTests as $test ) {
@ -1296,7 +1273,11 @@ class ApiBaseTest extends ApiTestCase {
$returnArray["\"$desc\" as integer"] = [
$test[0],
[ ApiBase::PARAM_TYPE => 'integer' ],
$test[1],
$test[1] ?? ApiUsageException::newWithMessage( null, [
'paramvalidator-badinteger',
Message::plaintextParam( 'myParam' ),
Message::plaintextParam( preg_replace( "/[\f\v\e\\0]/", '<27>', $test[0] ) ),
], 'badinteger' ),
$warnings,
];
}

View file

@ -1,19 +0,0 @@
<?php
/**
* @group API
* @group medium
*
* @covers ApiFeedContributions
*/
class ApiFeedContributionsTest extends ApiTestCase {
public function testInvalidExternalUser() {
$this->expectException( ApiUsageException::class );
$this->expectExceptionMessage( 'Invalid value ">" for user parameter "user"' );
$this->doApiRequest( [
'action' => 'feedcontributions',
'user' => '>'
] );
}
}

View file

@ -459,7 +459,7 @@ class ApiMainTest extends ApiTestCase {
], null, null, new User );
$this->fail( 'Expected exception not thrown' );
} catch ( ApiUsageException $e ) {
$this->assertTrue( self::apiExceptionHasCode( $e, 'too-many-titles' ), 'sanity check' );
$this->assertTrue( self::apiExceptionHasCode( $e, 'toomanyvalues' ), 'sanity check' );
}
// Now test that the assert happens first
@ -1170,6 +1170,6 @@ class ApiMainTest extends ApiTestCase {
$this->assertIsArray( $data );
$this->assertArrayHasKey( 'error', $data );
$this->assertArrayHasKey( 'code', $data['error'] );
$this->assertSame( 'unknown_formatversion', $data['error']['code'] );
$this->assertSame( 'badvalue', $data['error']['code'] );
}
}

View file

@ -115,7 +115,7 @@ abstract class ApiUploadTestCase extends ApiTestCase {
'type' => $type,
'tmp_name' => $tmpName,
'size' => $size,
'error' => null
'error' => UPLOAD_ERR_OK,
];
return true;

View file

@ -0,0 +1,289 @@
<?php
namespace MediaWiki\Api\Validator;
use ApiBase;
use ApiMain;
use ApiMessage;
use ApiQueryBase;
use ApiUploadTestCase;
use FauxRequest;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\TestingAccessWrapper;
/**
* @covers MediaWiki\Api\Validator\ApiParamValidatorCallbacks
* @group API
* @group medium
*/
class ApiParamValidatorCallbacksTest extends ApiUploadTestCase {
private function getCallbacks( FauxRequest $request ) : array {
$context = $this->apiContext->newTestContext( $request, $this->getTestUser()->getUser() );
$main = new ApiMain( $context );
return [ new ApiParamValidatorCallbacks( $main ), $main ];
}
private function filePath( $fileName ) {
return __DIR__ . '/../../../data/media/' . $fileName;
}
public function testHasParam() : void {
[ $callbacks, $main ] = $this->getCallbacks( new FauxRequest( [
'foo' => '1',
'bar' => '',
] ) );
$this->assertTrue( $callbacks->hasParam( 'foo', [] ) );
$this->assertTrue( $callbacks->hasParam( 'bar', [] ) );
$this->assertFalse( $callbacks->hasParam( 'baz', [] ) );
$this->assertSame(
[ 'foo', 'bar', 'baz' ],
TestingAccessWrapper::newFromObject( $main )->getParamsUsed()
);
}
/**
* @dataProvider provideGetValue
* @param string|null $data Value from request
* @param mixed $default For getValue()
* @param mixed $expect Expected return value
* @param bool $normalized Whether handleParamNormalization is called
*/
public function testGetValue( ?string $data, $default, $expect, bool $normalized = false ) : void {
[ $callbacks, $main ] = $this->getCallbacks( new FauxRequest( [ 'test' => $data ] ) );
$module = $this->getMockBuilder( ApiBase::class )
->setConstructorArgs( [ $main, 'testmodule' ] )
->setMethods( [ 'handleParamNormalization' ] )
->getMockForAbstractClass();
$options = [ 'module' => $module ];
if ( $normalized ) {
$module->expects( $this->once() )->method( 'handleParamNormalization' )
->with(
$this->identicalTo( 'test' ),
$this->identicalTo( $expect ),
$this->identicalTo( $data ?? $default )
);
} else {
$module->expects( $this->never() )->method( 'handleParamNormalization' );
}
$this->assertSame( $expect, $callbacks->getValue( 'test', $default, $options ) );
$this->assertSame( [ 'test' ], TestingAccessWrapper::newFromObject( $main )->getParamsUsed() );
}
public function provideGetValue() {
$obj = (object)[];
return [
'Basic test' => [ 'foo', 'bar', 'foo', false ],
'Default value' => [ null, 1234, 1234, false ],
'Default value (2)' => [ null, $obj, $obj, false ],
'No default value' => [ null, null, null, false ],
'Multi separator' => [ "\x1ffoo\x1fbar", 1234, "\x1ffoo\x1fbar", false ],
'Normalized' => [ "\x1ffoo\x1fba\u{0301}r", 1234, "\x1ffoo\x1fbár", true ],
];
}
private function setupUploads() : void {
$fileName = 'TestUploadStash.jpg';
$mimeType = 'image/jpeg';
$filePath = $this->filePath( 'yuv420.jpg' );
$this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath );
$_FILES['file2'] = [
'name' => '',
'type' => '',
'tmp_name' => '',
'size' => 0,
'error' => UPLOAD_ERR_NO_FILE,
];
$_FILES['file3'] = [
'name' => 'xxx.png',
'type' => '',
'tmp_name' => '',
'size' => 0,
'error' => UPLOAD_ERR_INI_SIZE,
];
}
public function testHasUpload() : void {
$this->setupUploads();
[ $callbacks, $main ] = $this->getCallbacks( new FauxRequest( [
'foo' => '1',
'bar' => '',
] ) );
$this->assertFalse( $callbacks->hasUpload( 'foo', [] ) );
$this->assertFalse( $callbacks->hasUpload( 'bar', [] ) );
$this->assertFalse( $callbacks->hasUpload( 'baz', [] ) );
$this->assertTrue( $callbacks->hasUpload( 'file', [] ) );
$this->assertTrue( $callbacks->hasUpload( 'file2', [] ) );
$this->assertTrue( $callbacks->hasUpload( 'file3', [] ) );
$this->assertSame(
[ 'foo', 'bar', 'baz', 'file', 'file2', 'file3' ],
TestingAccessWrapper::newFromObject( $main )->getParamsUsed()
);
}
public function testGetUploadedFile() : void {
$this->setupUploads();
[ $callbacks, $main ] = $this->getCallbacks( new FauxRequest( [
'foo' => '1',
'bar' => '',
] ) );
$this->assertNull( $callbacks->getUploadedFile( 'foo', [] ) );
$this->assertNull( $callbacks->getUploadedFile( 'bar', [] ) );
$this->assertNull( $callbacks->getUploadedFile( 'baz', [] ) );
$file = $callbacks->getUploadedFile( 'file', [] );
$this->assertInstanceOf( \Psr\Http\Message\UploadedFileInterface::class, $file );
$this->assertSame( UPLOAD_ERR_OK, $file->getError() );
$this->assertSame( 'TestUploadStash.jpg', $file->getClientFilename() );
$file = $callbacks->getUploadedFile( 'file2', [] );
$this->assertInstanceOf( \Psr\Http\Message\UploadedFileInterface::class, $file );
$this->assertSame( UPLOAD_ERR_NO_FILE, $file->getError() );
$file = $callbacks->getUploadedFile( 'file3', [] );
$this->assertInstanceOf( \Psr\Http\Message\UploadedFileInterface::class, $file );
$this->assertSame( UPLOAD_ERR_INI_SIZE, $file->getError() );
}
/**
* @dataProvider provideRecordCondition
* @param DataMessageValue $message
* @param ApiMessage|null $expect
* @param bool $sensitive
*/
public function testRecordCondition(
DataMessageValue $message, ?ApiMessage $expect, bool $sensitive = false
) : void {
[ $callbacks, $main ] = $this->getCallbacks( new FauxRequest( [ 'testparam' => 'testvalue' ] ) );
$query = $main->getModuleFromPath( 'query' );
$warnings = [];
$module = $this->getMockBuilder( ApiQueryBase::class )
->setConstructorArgs( [ $query, 'test' ] )
->setMethods( [ 'addWarning' ] )
->getMockForAbstractClass();
$module->method( 'addWarning' )->willReturnCallback(
function ( $msg, $code, $data ) use ( &$warnings ) {
$warnings[] = [ $msg, $code, $data ];
}
);
$query->getModuleManager()->addModule( 'test', 'meta', [
'class' => get_class( $module ),
'factory' => function () use ( $module ) {
return $module;
}
] );
$callbacks->recordCondition( $message, 'testparam', 'testvalue', [], [ 'module' => $module ] );
if ( $expect ) {
$this->assertNotCount( 0, $warnings );
$this->assertSame(
$expect->inLanguage( 'qqx' )->plain(),
$warnings[0][0]->inLanguage( 'qqx' )->plain()
);
$this->assertSame( $expect->getApiCode(), $warnings[0][1] );
$this->assertSame( $expect->getApiData(), $warnings[0][2] );
} else {
$this->assertEmpty( $warnings );
}
$this->assertSame(
$sensitive ? [ 'testparam' ] : [],
TestingAccessWrapper::newFromObject( $main )->getSensitiveParams()
);
}
public function provideRecordCondition() : \Generator {
yield 'Deprecated param' => [
DataMessageValue::new(
'paramvalidator-param-deprecated', [],
'param-deprecated',
[ 'data' => true ]
)->plaintextParams( 'XXtestparam', 'XXtestvalue' ),
ApiMessage::create(
'paramvalidator-param-deprecated',
'deprecation',
[ 'data' => true, 'feature' => 'action=query&meta=test&testparam' ]
)->plaintextParams( 'XXtestparam', 'XXtestvalue' )
];
yield 'Deprecated value' => [
DataMessageValue::new(
'paramvalidator-deprecated-value', [],
'deprecated-value'
)->plaintextParams( 'XXtestparam', 'XXtestvalue' ),
ApiMessage::create(
'paramvalidator-deprecated-value',
'deprecation',
[ 'feature' => 'action=query&meta=test&testparam=testvalue' ]
)->plaintextParams( 'XXtestparam', 'XXtestvalue' )
];
yield 'Deprecated value with custom MessageValue' => [
DataMessageValue::new(
'some-custom-message-value', [],
'deprecated-value',
[ 'xyz' => 123 ]
)->plaintextParams( 'XXtestparam', 'XXtestvalue', 'foobar' ),
ApiMessage::create(
'some-custom-message-value',
'deprecation',
[ 'xyz' => 123, 'feature' => 'action=query&meta=test&testparam=testvalue' ]
)->plaintextParams( 'XXtestparam', 'XXtestvalue', 'foobar' )
];
// See ApiParamValidator::normalizeSettings()
yield 'Deprecated value with custom Message' => [
DataMessageValue::new(
'some-custom-message', [],
'deprecated-value',
[ '💩' => 'back-compat' ]
)->plaintextParams( 'XXtestparam', 'XXtestvalue', 'foobar' ),
ApiMessage::create(
'some-custom-message',
'deprecation',
[ 'feature' => 'action=query&meta=test&testparam=testvalue' ]
)->plaintextParams( 'foobar' )
];
yield 'Sensitive param' => [
DataMessageValue::new( 'paramvalidator-param-sensitive', [], 'param-sensitive' )
->plaintextParams( 'XXtestparam', 'XXtestvalue' ),
null,
true
];
yield 'Arbitrary warning' => [
DataMessageValue::new( 'some-warning', [], 'some-code', [ 'some-data' ] )
->plaintextParams( 'XXtestparam', 'XXtestvalue', 'foobar' ),
ApiMessage::create( 'some-warning', 'some-code', [ 'some-data' ] )
->plaintextParams( 'XXtestparam', 'XXtestvalue', 'foobar' ),
];
}
public function testUseHighLimits() : void {
$context = $this->apiContext->newTestContext( new FauxRequest, $this->getTestUser()->getUser() );
$main = $this->getMockBuilder( ApiMain::class )
->setConstructorArgs( [ $context ] )
->setMethods( [ 'canApiHighLimits' ] )
->getMock();
$main->method( 'canApiHighLimits' )->will( $this->onConsecutiveCalls( true, false ) );
$callbacks = new ApiParamValidatorCallbacks( $main );
$this->assertTrue( $callbacks->useHighLimits( [] ) );
$this->assertFalse( $callbacks->useHighLimits( [] ) );
}
}

View file

@ -0,0 +1,327 @@
<?php
namespace MediaWiki\Api\Validator;
use ApiBase;
use ApiMain;
use ApiMessage;
use ApiTestCase;
use ApiUsageException;
use FauxRequest;
use MediaWiki\MediaWikiServices;
use Message;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\Message\MessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef\EnumDef;
use Wikimedia\ParamValidator\TypeDef\IntegerDef;
use Wikimedia\TestingAccessWrapper;
/**
* @covers MediaWiki\Api\Validator\ApiParamValidator
* @group API
* @group medium
*/
class ApiParamValidatorTest extends ApiTestCase {
private function getValidator( FauxRequest $request ) : array {
$context = $this->apiContext->newTestContext( $request, $this->getTestUser()->getUser() );
$main = new ApiMain( $context );
return [
new ApiParamValidator( $main, MediaWikiServices::getInstance()->getObjectFactory() ),
$main
];
}
public function testKnwonTypes() : void {
[ $validator ] = $this->getValidator( new FauxRequest( [] ) );
$this->assertSame(
[
'boolean', 'enum', 'integer', 'limit', 'namespace', 'NULL', 'password', 'string', 'submodule',
'tags', 'text', 'timestamp', 'user', 'upload',
],
$validator->knownTypes()
);
}
/**
* @dataProvider provideNormalizeSettings
* @param array|mixed $settings
* @param array $expect
*/
public function testNormalizeSettings( $settings, array $expect ) : void {
[ $validator ] = $this->getValidator( new FauxRequest( [] ) );
$this->assertEquals( $expect, $validator->normalizeSettings( $settings ) );
}
public function provideNormalizeSettings() : array {
return [
'Basic test' => [
[],
[
ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES => true,
IntegerDef::PARAM_IGNORE_RANGE => true,
ParamValidator::PARAM_TYPE => 'NULL',
],
],
'Explicit ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES' => [
[
ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES => false,
],
[
ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES => false,
IntegerDef::PARAM_IGNORE_RANGE => true,
ParamValidator::PARAM_TYPE => 'NULL',
],
],
'Explicit IntegerDef::PARAM_IGNORE_RANGE' => [
[
IntegerDef::PARAM_IGNORE_RANGE => false,
],
[
ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES => true,
IntegerDef::PARAM_IGNORE_RANGE => false,
ParamValidator::PARAM_TYPE => 'NULL',
],
],
'Handle ApiBase::PARAM_RANGE_ENFORCE' => [
[
ApiBase::PARAM_RANGE_ENFORCE => true,
],
[
ApiBase::PARAM_RANGE_ENFORCE => true,
ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES => true,
IntegerDef::PARAM_IGNORE_RANGE => false,
ParamValidator::PARAM_TYPE => 'NULL',
],
],
'Handle EnumDef::PARAM_DEPRECATED_VALUES, null' => [
[
EnumDef::PARAM_DEPRECATED_VALUES => [
'null' => null,
'true' => true,
'string' => 'some-message',
'array' => [ 'some-message', 'with', 'params' ],
'Message' => ApiMessage::create(
[ 'api-message', 'with', 'params' ], 'somecode', [ 'some-data' ]
),
'MessageValue' => MessageValue::new( 'message-value', [ 'with', 'params' ] ),
'DataMessageValue' => DataMessageValue::new(
'data-message-value', [ 'with', 'params' ], 'somecode', [ 'some-data' ]
),
],
],
[
ParamValidator::PARAM_IGNORE_UNRECOGNIZED_VALUES => true,
IntegerDef::PARAM_IGNORE_RANGE => true,
EnumDef::PARAM_DEPRECATED_VALUES => [
'null' => null,
'true' => true,
'string' => DataMessageValue::new( 'some-message', [], 'bogus', [ '💩' => 'back-compat' ] ),
'array' => DataMessageValue::new(
'some-message', [ 'with', 'params' ], 'bogus', [ '💩' => 'back-compat' ]
),
'Message' => DataMessageValue::new(
'api-message', [ 'with', 'params' ], 'bogus', [ '💩' => 'back-compat' ]
),
'MessageValue' => MessageValue::new( 'message-value', [ 'with', 'params' ] ),
'DataMessageValue' => DataMessageValue::new(
'data-message-value', [ 'with', 'params' ], 'somecode', [ 'some-data' ]
),
],
ParamValidator::PARAM_TYPE => 'NULL',
],
],
];
}
/**
* @dataProvider provideGetValue
* @param string|null $data Request value
* @param mixed $settings Settings
* @param mixed $expect Expected value, or an expected ApiUsageException
*/
public function testGetValue( ?string $data, $settings, $expect ) : void {
[ $validator, $main ] = $this->getValidator( new FauxRequest( [ 'aptest' => $data ] ) );
$module = $main->getModuleFromPath( 'query+allpages' );
if ( $expect instanceof ApiUsageException ) {
try {
$validator->getValue( $module, 'test', $settings, [] );
$this->fail( 'Expected exception not thrown' );
} catch ( ApiUsageException $e ) {
$this->assertSame( $module->getModulePath(), $e->getModulePath() );
$this->assertEquals( $expect->getStatusValue(), $e->getStatusValue() );
}
} else {
$this->assertEquals( $expect, $validator->getValue( $module, 'test', $settings, [] ) );
}
}
public function provideGetValue() : array {
return [
'Basic test' => [
'1234',
[
ParamValidator::PARAM_TYPE => 'integer',
],
1234
],
'Test for default' => [
null,
1234,
1234
],
'Test no value' => [
null,
[
ParamValidator::PARAM_TYPE => 'integer',
],
null,
],
'Test boolean (false)' => [
null,
false,
null,
],
'Test boolean (true)' => [
'',
false,
true,
],
'Validation failure' => [
'xyz',
[
ParamValidator::PARAM_TYPE => 'integer',
],
ApiUsageException::newWithMessage( null, [
'paramvalidator-badinteger',
Message::plaintextParam( 'aptest' ),
Message::plaintextParam( 'xyz' ),
], 'badinteger' ),
],
];
}
/**
* @dataProvider provideValidateValue
* @param mixed $value Value to validate
* @param mixed $settings Settings
* @param mixed $value Value to validate
* @param mixed $expect Expected value, or an expected ApiUsageException
*/
public function testValidateValue( $value, $settings, $expect ) : void {
[ $validator, $main ] = $this->getValidator( new FauxRequest() );
$module = $main->getModuleFromPath( 'query+allpages' );
if ( $expect instanceof ApiUsageException ) {
try {
$validator->validateValue( $module, 'test', $value, $settings, [] );
$this->fail( 'Expected exception not thrown' );
} catch ( ApiUsageException $e ) {
$this->assertSame( $module->getModulePath(), $e->getModulePath() );
$this->assertEquals( $expect->getStatusValue(), $e->getStatusValue() );
}
} else {
$this->assertEquals(
$expect,
$validator->validateValue( $module, 'test', $value, $settings, [] )
);
}
}
public function provideValidateValue() : array {
return [
'Basic test' => [
1234,
[
ParamValidator::PARAM_TYPE => 'integer',
],
1234
],
'Validation failure' => [
1234,
[
ParamValidator::PARAM_TYPE => 'integer',
IntegerDef::PARAM_IGNORE_RANGE => false,
IntegerDef::PARAM_MAX => 10,
],
ApiUsageException::newWithMessage( null, [
'paramvalidator-outofrange-max',
Message::plaintextParam( 'aptest' ),
Message::plaintextParam( 1234 ),
Message::numParam( '' ),
Message::numParam( 10 ),
], 'outofrange', [ 'min' => null, 'curmax' => 10, 'max' => 10, 'highmax' => 10 ] ),
],
];
}
public function testGetParamInfo() {
[ $validator, $main ] = $this->getValidator( new FauxRequest() );
$module = $main->getModuleFromPath( 'query+allpages' );
$dummy = (object)[];
$settings = [
'foo' => (object)[],
];
$options = [
'bar' => (object)[],
];
$mock = $this->getMockBuilder( ParamValidator::class )
->disableOriginalConstructor()
->setMethods( [ 'getParamInfo' ] )
->getMock();
$mock->expects( $this->once() )->method( 'getParamInfo' )
->with(
$this->identicalTo( 'aptest' ),
$this->identicalTo( $settings ),
$this->identicalTo( $options + [ 'module' => $module ] )
)
->willReturn( [ $dummy ] );
TestingAccessWrapper::newFromObject( $validator )->paramValidator = $mock;
$this->assertSame( [ $dummy ], $validator->getParamInfo( $module, 'test', $settings, $options ) );
}
public function testGetHelpInfo() {
[ $validator, $main ] = $this->getValidator( new FauxRequest() );
$module = $main->getModuleFromPath( 'query+allpages' );
$settings = [
'foo' => (object)[],
];
$options = [
'bar' => (object)[],
];
$mock = $this->getMockBuilder( ParamValidator::class )
->disableOriginalConstructor()
->setMethods( [ 'getHelpInfo' ] )
->getMock();
$mock->expects( $this->once() )->method( 'getHelpInfo' )
->with(
$this->identicalTo( 'aptest' ),
$this->identicalTo( $settings ),
$this->identicalTo( $options + [ 'module' => $module ] )
)
->willReturn( [
'mv1' => MessageValue::new( 'parentheses', [ 'foobar' ] ),
'mv2' => MessageValue::new( 'paramvalidator-help-continue' ),
] );
TestingAccessWrapper::newFromObject( $validator )->paramValidator = $mock;
$ret = $validator->getHelpInfo( $module, 'test', $settings, $options );
$this->assertArrayHasKey( 'mv1', $ret );
$this->assertInstanceOf( Message::class, $ret['mv1'] );
$this->assertEquals( '(parentheses: foobar)', $ret['mv1']->inLanguage( 'qqx' )->plain() );
$this->assertArrayHasKey( 'mv2', $ret );
$this->assertInstanceOf( Message::class, $ret['mv2'] );
$this->assertEquals(
[ 'api-help-param-continue', 'paramvalidator-help-continue' ],
$ret['mv2']->getKeysToTry()
);
$this->assertCount( 2, $ret );
}
}

View file

@ -0,0 +1,206 @@
<?php
namespace MediaWiki\Api\Validator;
use ApiMain;
use ApiModuleManager;
use MockApi;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\ParamValidator\TypeDef\TypeDefTestCase;
use Wikimedia\ParamValidator\ValidationException;
use Wikimedia\TestingAccessWrapper;
/**
* @covers MediaWiki\Api\Validator\SubmoduleDef
*/
class SubmoduleDefTest extends TypeDefTestCase {
protected static $testClass = SubmoduleDef::class;
private function mockApi() {
$api = $this->getMockBuilder( MockApi::class )
->setMethods( [ 'getModuleManager' ] )
->getMock();
$w = TestingAccessWrapper::newFromObject( $api );
$w->mModuleName = 'testmod';
$w->mMainModule = new ApiMain;
$w->mModulePrefix = 'tt';
$w->mMainModule->getModuleManager()->addModule( 'testmod', 'action', [
'class' => MockApi::class,
'factory' => function () use ( $api ) {
return $api;
},
] );
$dep = $this->getMockBuilder( MockApi::class )
->setMethods( [ 'isDeprecated' ] )
->getMock();
$dep->method( 'isDeprecated' )->willReturn( true );
$int = $this->getMockBuilder( MockApi::class )
->setMethods( [ 'isInternal' ] )
->getMock();
$int->method( 'isInternal' )->willReturn( true );
$depint = $this->getMockBuilder( MockApi::class )
->setMethods( [ 'isDeprecated', 'isInternal' ] )
->getMock();
$depint->method( 'isDeprecated' )->willReturn( true );
$depint->method( 'isInternal' )->willReturn( true );
$manager = new ApiModuleManager( $api );
$api->method( 'getModuleManager' )->willReturn( $manager );
$manager->addModule( 'mod1', 'test', MockApi::class );
$manager->addModule( 'mod2', 'test', MockApi::class );
$manager->addModule( 'dep', 'test', [
'class' => MockApi::class,
'factory' => function () use ( $dep ) {
return $dep;
},
] );
$manager->addModule( 'depint', 'test', [
'class' => MockApi::class,
'factory' => function () use ( $depint ) {
return $depint;
},
] );
$manager->addModule( 'int', 'test', [
'class' => MockApi::class,
'factory' => function () use ( $int ) {
return $int;
},
] );
$manager->addModule( 'recurse', 'test', [
'class' => MockApi::class,
'factory' => function () use ( $api ) {
return $api;
},
] );
$manager->addModule( 'mod3', 'xyz', MockApi::class );
$this->assertSame( $api, $api->getModuleFromPath( 'testmod' ), 'sanity check' );
$this->assertSame( $dep, $api->getModuleFromPath( 'testmod+dep' ), 'sanity check' );
$this->assertSame( $int, $api->getModuleFromPath( 'testmod+int' ), 'sanity check' );
$this->assertSame( $depint, $api->getModuleFromPath( 'testmod+depint' ), 'sanity check' );
return $api;
}
public function provideValidate() {
$opts = [
'module' => $this->mockApi(),
];
$map = [
SubmoduleDef::PARAM_SUBMODULE_MAP => [
'mod2' => 'testmod+mod1',
'mod3' => 'testmod+mod3',
],
];
return [
'Basic' => [ 'mod1', 'mod1', [], $opts ],
'Nonexistent submodule' => [
'mod3',
new ValidationException(
DataMessageValue::new( 'paramvalidator-badvalue', [], 'badvalue', [] ), 'test', 'mod3', []
),
[],
$opts,
],
'Mapped' => [ 'mod3', 'mod3', $map, $opts ],
'Mapped, not in map' => [
'mod1',
new ValidationException(
DataMessageValue::new( 'paramvalidator-badvalue', [], 'badvalue', [] ), 'test', 'mod1', $map
),
$map,
$opts,
],
];
}
public function provideGetEnumValues() {
$opts = [
'module' => $this->mockApi(),
];
return [
'Basic test' => [
[ ParamValidator::PARAM_TYPE => 'submodule' ],
[ 'mod1', 'mod2', 'dep', 'depint', 'int', 'recurse' ],
$opts,
],
'Mapped' => [
[
ParamValidator::PARAM_TYPE => 'submodule',
SubmoduleDef::PARAM_SUBMODULE_MAP => [
'mod2' => 'test+mod1',
'mod3' => 'test+mod3',
]
],
[ 'mod2', 'mod3' ],
$opts,
],
];
}
public function provideGetInfo() {
$opts = [
'module' => $this->mockApi(),
];
return [
'Basic' => [
[],
[
'type' => [ 'mod1', 'mod2', 'recurse', 'dep', 'int', 'depint' ],
'submodules' => [
'mod1' => 'testmod+mod1',
'mod2' => 'testmod+mod2',
'recurse' => 'testmod+recurse',
'dep' => 'testmod+dep',
'int' => 'testmod+int',
'depint' => 'testmod+depint',
],
'deprecatedvalues' => [ 'dep', 'depint' ],
'internalvalues' => [ 'depint', 'int' ],
],
[
// phpcs:ignore Generic.Files.LineLength.TooLong
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>1</text><list listType="comma"><text>[[Special:ApiHelp/testmod+mod1|&lt;span dir=&quot;ltr&quot; lang=&quot;en&quot;&gt;mod1&lt;/span&gt;]]</text><text>[[Special:ApiHelp/testmod+mod2|&lt;span dir=&quot;ltr&quot; lang=&quot;en&quot;&gt;mod2&lt;/span&gt;]]</text><text>[[Special:ApiHelp/testmod+recurse|&lt;span dir=&quot;ltr&quot; lang=&quot;en&quot;&gt;recurse&lt;/span&gt;]]</text><text>[[Special:ApiHelp/testmod+dep|&lt;span dir=&quot;ltr&quot; lang=&quot;en&quot; class=&quot;apihelp-deprecated-value&quot;&gt;dep&lt;/span&gt;]]</text><text>[[Special:ApiHelp/testmod+int|&lt;span dir=&quot;ltr&quot; lang=&quot;en&quot; class=&quot;apihelp-internal-value&quot;&gt;int&lt;/span&gt;]]</text><text>[[Special:ApiHelp/testmod+depint|&lt;span dir=&quot;ltr&quot; lang=&quot;en&quot; class=&quot;apihelp-deprecated-value apihelp-internal-value&quot;&gt;depint&lt;/span&gt;]]</text></list><num>6</num></message>',
ParamValidator::PARAM_ISMULTI => null,
],
$opts,
],
'Mapped' => [
[
ParamValidator::PARAM_DEFAULT => 'mod3|mod4',
ParamValidator::PARAM_ISMULTI => true,
SubmoduleDef::PARAM_SUBMODULE_PARAM_PREFIX => 'g',
SubmoduleDef::PARAM_SUBMODULE_MAP => [
'xyz' => 'testmod+dep',
'mod3' => 'testmod+mod3',
'mod4' => 'testmod+mod4', // doesn't exist
],
],
[
'type' => [ 'mod3', 'mod4', 'xyz' ],
'submodules' => [
'mod3' => 'testmod+mod3',
'mod4' => 'testmod+mod4',
'xyz' => 'testmod+dep',
],
'submoduleparamprefix' => 'g',
'deprecatedvalues' => [ 'xyz' ],
],
[
// phpcs:ignore Generic.Files.LineLength.TooLong
ParamValidator::PARAM_TYPE => '<message key="paramvalidator-help-type-enum"><text>2</text><list listType="comma"><text>[[Special:ApiHelp/testmod+mod3|&lt;span dir=&quot;ltr&quot; lang=&quot;en&quot;&gt;mod3&lt;/span&gt;]]</text><text>[[Special:ApiHelp/testmod+mod4|&lt;span dir=&quot;ltr&quot; lang=&quot;en&quot;&gt;mod4&lt;/span&gt;]]</text><text>[[Special:ApiHelp/testmod+dep|&lt;span dir=&quot;ltr&quot; lang=&quot;en&quot; class=&quot;apihelp-deprecated-value&quot;&gt;xyz&lt;/span&gt;]]</text></list><num>3</num></message>',
ParamValidator::PARAM_ISMULTI => null,
],
$opts,
],
];
}
}