Trying to get away with returning a single code and parameter-list that was supposed to represent both human-readable and machine-readable data was a mistake. This patch converts it to use DataMessageValue, which represents the two separately and also provides guidance for supplying translations of all the error codes. This also eliminates the "describeSettings()" method that was trying to serve multiple use cases (in terms of the Action API, action=paraminfo and action=help). It's replaced by two methods that each serve one of the use cases. Also some of the functionality was moved out of the TypeDef base class into ParamValidator, to better match where the constants themselves live. Also I wound up creating a NumericDef base class so FloatDef can share the same range-checking logic that IntegerDef has. I probably should have done that as a separate patch, but untangling it now would be too much work. Bug: T235801 Change-Id: Iea6d4a1d05bb4b92d60415b0f03ff9d3dc99a80b
169 lines
5.3 KiB
PHP
169 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Rest\Validator;
|
|
|
|
use MediaWiki\Permissions\PermissionManager;
|
|
use MediaWiki\Rest\Handler;
|
|
use MediaWiki\Rest\HttpException;
|
|
use MediaWiki\Rest\LocalizedHttpException;
|
|
use MediaWiki\Rest\RequestInterface;
|
|
use MediaWiki\User\UserIdentity;
|
|
use Wikimedia\ObjectFactory;
|
|
use Wikimedia\ParamValidator\ParamValidator;
|
|
use Wikimedia\ParamValidator\TypeDef\BooleanDef;
|
|
use Wikimedia\ParamValidator\TypeDef\EnumDef;
|
|
use Wikimedia\ParamValidator\TypeDef\FloatDef;
|
|
use Wikimedia\ParamValidator\TypeDef\IntegerDef;
|
|
use Wikimedia\ParamValidator\TypeDef\PasswordDef;
|
|
use Wikimedia\ParamValidator\TypeDef\StringDef;
|
|
use Wikimedia\ParamValidator\TypeDef\TimestampDef;
|
|
use Wikimedia\ParamValidator\TypeDef\UploadDef;
|
|
use Wikimedia\ParamValidator\ValidationException;
|
|
|
|
/**
|
|
* Wrapper for ParamValidator
|
|
*
|
|
* It's intended to be used in the REST API classes by composition.
|
|
*
|
|
* @since 1.34
|
|
*/
|
|
class Validator {
|
|
|
|
/** @var array Type defs for ParamValidator */
|
|
private const TYPE_DEFS = [
|
|
'boolean' => [ 'class' => BooleanDef::class ],
|
|
'enum' => [ 'class' => EnumDef::class ],
|
|
'integer' => [ 'class' => IntegerDef::class ],
|
|
'float' => [ 'class' => FloatDef::class ],
|
|
'double' => [ 'class' => FloatDef::class ],
|
|
'NULL' => [
|
|
'class' => StringDef::class,
|
|
'args' => [ [
|
|
'allowEmptyWhenRequired' => true,
|
|
] ],
|
|
],
|
|
'password' => [ 'class' => PasswordDef::class ],
|
|
'string' => [ 'class' => StringDef::class ],
|
|
'timestamp' => [ 'class' => TimestampDef::class ],
|
|
'upload' => [ 'class' => UploadDef::class ],
|
|
];
|
|
|
|
/** @var string[] HTTP request methods that we expect never to have a payload */
|
|
private const NO_BODY_METHODS = [ 'GET', 'HEAD', 'DELETE' ];
|
|
|
|
/** @var string[] HTTP request methods that we expect always to have a payload */
|
|
private const BODY_METHODS = [ 'POST', 'PUT' ];
|
|
|
|
/** @var string[] Content types handled via $_POST */
|
|
private const FORM_DATA_CONTENT_TYPES = [
|
|
'application/x-www-form-urlencoded',
|
|
'multipart/form-data',
|
|
];
|
|
|
|
/** @var ParamValidator */
|
|
private $paramValidator;
|
|
|
|
/**
|
|
* @param ObjectFactory $objectFactory
|
|
* @param PermissionManager $permissionManager
|
|
* @param RequestInterface $request
|
|
* @param UserIdentity $user
|
|
* @internal
|
|
*/
|
|
public function __construct(
|
|
ObjectFactory $objectFactory,
|
|
PermissionManager $permissionManager,
|
|
RequestInterface $request,
|
|
UserIdentity $user
|
|
) {
|
|
$this->paramValidator = new ParamValidator(
|
|
new ParamValidatorCallbacks( $permissionManager, $request, $user ),
|
|
$objectFactory,
|
|
[
|
|
'typeDefs' => self::TYPE_DEFS,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Validate parameters
|
|
* @param array[] $paramSettings Parameter settings
|
|
* @return array Validated parameters
|
|
* @throws HttpException on validaton failure
|
|
*/
|
|
public function validateParams( array $paramSettings ) {
|
|
$validatedParams = [];
|
|
foreach ( $paramSettings as $name => $settings ) {
|
|
try {
|
|
$validatedParams[$name] = $this->paramValidator->getValue( $name, $settings, [
|
|
'source' => $settings[Handler::PARAM_SOURCE] ?? 'unspecified',
|
|
] );
|
|
} catch ( ValidationException $e ) {
|
|
throw new LocalizedHttpException( $e->getFailureMessage(), 400, [
|
|
'error' => 'parameter-validation-failed',
|
|
'name' => $e->getParamName(),
|
|
'value' => $e->getParamValue(),
|
|
'failureCode' => $e->getFailureMessage()->getCode(),
|
|
'failureData' => $e->getFailureMessage()->getData(),
|
|
] );
|
|
}
|
|
}
|
|
return $validatedParams;
|
|
}
|
|
|
|
/**
|
|
* Validate the body of a request.
|
|
*
|
|
* This may return a data structure representing the parsed body. When used
|
|
* in the context of Handler::validateParams(), the returned value will be
|
|
* available to the handler via Handler::getValidatedBody().
|
|
*
|
|
* @param RequestInterface $request
|
|
* @param Handler $handler Used to call getBodyValidator()
|
|
* @return mixed May be null
|
|
* @throws HttpException on validation failure
|
|
*/
|
|
public function validateBody( RequestInterface $request, Handler $handler ) {
|
|
$method = strtoupper( trim( $request->getMethod() ) );
|
|
|
|
// If the method should never have a body, don't bother validating.
|
|
if ( in_array( $method, self::NO_BODY_METHODS, true ) ) {
|
|
return null;
|
|
}
|
|
|
|
// Get the content type
|
|
list( $ct ) = explode( ';', $request->getHeaderLine( 'Content-Type' ), 2 );
|
|
$ct = strtolower( trim( $ct ) );
|
|
if ( $ct === '' ) {
|
|
// No Content-Type was supplied. RFC 7231 § 3.1.1.5 allows this, but since it's probably a
|
|
// client error let's return a 415. But don't 415 for unknown methods and an empty body.
|
|
if ( !in_array( $method, self::BODY_METHODS, true ) ) {
|
|
$body = $request->getBody();
|
|
$size = $body->getSize();
|
|
if ( $size === null ) {
|
|
// No size available. Try reading 1 byte.
|
|
if ( $body->isSeekable() ) {
|
|
$body->rewind();
|
|
}
|
|
$size = $body->read( 1 ) === '' ? 0 : 1;
|
|
}
|
|
if ( $size === 0 ) {
|
|
return null;
|
|
}
|
|
}
|
|
throw new HttpException( "A Content-Type header must be supplied with a request payload.", 415, [
|
|
'error' => 'no-content-type',
|
|
] );
|
|
}
|
|
|
|
// Form data is parsed into $_POST and $_FILES by PHP and from there is accessed as parameters,
|
|
// don't bother trying to handle these via BodyValidator too.
|
|
if ( in_array( $ct, self::FORM_DATA_CONTENT_TYPES, true ) ) {
|
|
return null;
|
|
}
|
|
|
|
// Validate the body. BodyValidator throws an HttpException on failure.
|
|
return $handler->getBodyValidator( $ct )->validateBody( $request );
|
|
}
|
|
|
|
}
|