rest: Use ParamValidator library, add BodyValidator
authorBrad Jorsch <bjorsch@wikimedia.org>
Wed, 12 Jun 2019 19:51:59 +0000 (15:51 -0400)
committerBrad Jorsch <bjorsch@wikimedia.org>
Wed, 4 Sep 2019 14:12:35 +0000 (10:12 -0400)
Parameter validation is based on parameter definitions like those in the
Action API, using the new ParamValidator library. Handlers should use
the provided Handler methods to access parameters rather than fetching
them directly from the RequestInterface.

Body validation allows the handler to have the (non-form-data) body of a
request parsed and validated. The only validator included in this patch
ignores the body entirely; future patches may implement validation for
JSON bodies based on JSON schemas, or the like.

Bug: T223239
Change-Id: I3c37ea2b432840514b6bff90007c8403989225d5

15 files changed:
includes/Rest/EntryPoint.php
includes/Rest/Handler.php
includes/Rest/Handler/HelloHandler.php
includes/Rest/HttpException.php
includes/Rest/ResponseFactory.php
includes/Rest/Router.php
includes/Rest/SimpleHandler.php
includes/Rest/Validator/BodyValidator.php [new file with mode: 0644]
includes/Rest/Validator/NullBodyValidator.php [new file with mode: 0644]
includes/Rest/Validator/ParamValidatorCallbacks.php [new file with mode: 0644]
includes/Rest/Validator/Validator.php [new file with mode: 0644]
tests/phpunit/includes/Rest/BasicAccess/MWBasicRequestAuthorizerTest.php
tests/phpunit/includes/Rest/EntryPointTest.php
tests/phpunit/unit/includes/Rest/Handler/HelloHandlerTest.php
tests/phpunit/unit/includes/Rest/RouterTest.php

index f28b4ea..ee3441e 100644 (file)
@@ -6,6 +6,7 @@ use ExtensionRegistry;
 use MediaWiki;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Rest\BasicAccess\MWBasicAuthorizer;
+use MediaWiki\Rest\Validator\Validator;
 use RequestContext;
 use Title;
 use WebResponse;
@@ -36,6 +37,7 @@ class EntryPoint {
 
                $services = MediaWikiServices::getInstance();
                $conf = $services->getMainConfig();
+               $objectFactory = $services->getObjectFactory();
 
                if ( !$conf->get( 'EnableRestAPI' ) ) {
                        wfHttpError( 403, 'Access Denied',
@@ -51,6 +53,9 @@ class EntryPoint {
                $authorizer = new MWBasicAuthorizer( $context->getUser(),
                        $services->getPermissionManager() );
 
+               // @phan-suppress-next-line PhanAccessMethodInternal
+               $restValidator = new Validator( $objectFactory, $request, RequestContext::getMain()->getUser() );
+
                global $IP;
                $router = new Router(
                        [ "$IP/includes/Rest/coreRoutes.json" ],
@@ -58,7 +63,9 @@ class EntryPoint {
                        $conf->get( 'RestPath' ),
                        $services->getLocalServerObjectCache(),
                        new ResponseFactory,
-                       $authorizer
+                       $authorizer,
+                       $objectFactory,
+                       $restValidator
                );
 
                $entryPoint = new self(
index c05d8e7..efe2b7e 100644 (file)
@@ -2,7 +2,18 @@
 
 namespace MediaWiki\Rest;
 
+use MediaWiki\Rest\Validator\BodyValidator;
+use MediaWiki\Rest\Validator\NullBodyValidator;
+use MediaWiki\Rest\Validator\Validator;
+
 abstract class Handler {
+
+       /**
+        * (string) ParamValidator constant to specify the source of the parameter.
+        * Value must be 'path', 'query', or 'post'.
+        */
+       const PARAM_SOURCE = 'rest-param-source';
+
        /** @var Router */
        private $router;
 
@@ -15,6 +26,12 @@ abstract class Handler {
        /** @var ResponseFactory */
        private $responseFactory;
 
+       /** @var array|null */
+       private $validatedParams;
+
+       /** @var mixed */
+       private $validatedBody;
+
        /**
         * Initialise with dependencies from the Router. This is called after construction.
         * @internal
@@ -68,6 +85,62 @@ abstract class Handler {
                return $this->responseFactory;
        }
 
+       /**
+        * Validate the request parameters/attributes and body. If there is a validation
+        * failure, a response with an error message should be returned or an
+        * HttpException should be thrown.
+        *
+        * @param Validator $restValidator
+        * @throws HttpException On validation failure.
+        */
+       public function validate( Validator $restValidator ) {
+               $validatedParams = $restValidator->validateParams( $this->getParamSettings() );
+               $validatedBody = $restValidator->validateBody( $this->request, $this );
+               $this->validatedParams = $validatedParams;
+               $this->validatedBody = $validatedBody;
+       }
+
+       /**
+        * Fetch ParamValidator settings for parameters
+        *
+        * Every setting must include self::PARAM_SOURCE to specify which part of
+        * the request is to contain the parameter.
+        *
+        * @return array[] Associative array mapping parameter names to
+        *  ParamValidator settings arrays
+        */
+       public function getParamSettings() {
+               return [];
+       }
+
+       /**
+        * Fetch the BodyValidator
+        * @param string $contentType Content type of the request.
+        * @return BodyValidator
+        */
+       public function getBodyValidator( $contentType ) {
+               return new NullBodyValidator();
+       }
+
+       /**
+        * Fetch the validated parameters
+        *
+        * @return array|null Array mapping parameter names to validated values,
+        *  or null if validateParams() was not called yet or validation failed.
+        */
+       public function getValidatedParams() {
+               return $this->validatedParams;
+       }
+
+       /**
+        * Fetch the validated body
+        * @return mixed Value returned by the body validator, or null if validateParams() was
+        *  not called yet, validation failed, there was no body, or the body was form data.
+        */
+       public function getValidatedBody() {
+               return $this->validatedBody;
+       }
+
        /**
         * The subclass should override this to provide the maximum last modified
         * timestamp for the current request. This is called before execute() in
index 34faee2..495b101 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace MediaWiki\Rest\Handler;
 
+use Wikimedia\ParamValidator\ParamValidator;
 use MediaWiki\Rest\SimpleHandler;
 
 /**
@@ -16,4 +17,14 @@ class HelloHandler extends SimpleHandler {
        public function needsWriteAccess() {
                return false;
        }
+
+       public function getParamSettings() {
+               return [
+                       'name' => [
+                               self::PARAM_SOURCE => 'path',
+                               ParamValidator::PARAM_TYPE => 'string',
+                               ParamValidator::PARAM_REQUIRED => true,
+                       ],
+               ];
+       }
 }
index ae6dde2..bcc414f 100644 (file)
@@ -8,7 +8,19 @@ namespace MediaWiki\Rest;
  * error response.
  */
 class HttpException extends \Exception {
-       public function __construct( $message, $code = 500 ) {
+
+       /** @var array|null */
+       private $errorData = null;
+
+       public function __construct( $message, $code = 500, $errorData = null ) {
                parent::__construct( $message, $code );
+               $this->errorData = $errorData;
+       }
+
+       /**
+        * @return array|null
+        */
+       public function getErrorData() {
+               return $this->errorData;
        }
 }
index d18cdb5..5e5a198 100644 (file)
@@ -175,8 +175,13 @@ class ResponseFactory {
        public function createFromException( $exception ) {
                if ( $exception instanceof HttpException ) {
                        // FIXME can HttpException represent 2xx or 3xx responses?
-                       $response = $this->createHttpError( $exception->getCode(),
-                               [ 'message' => $exception->getMessage() ] );
+                       $response = $this->createHttpError(
+                               $exception->getCode(),
+                               array_merge(
+                                       [ 'message' => $exception->getMessage() ],
+                                       (array)$exception->getErrorData()
+                               )
+                       );
                } else {
                        $response = $this->createHttpError( 500, [
                                'message' => 'Error: exception of type ' . get_class( $exception ),
index 961da01..a520130 100644 (file)
@@ -6,6 +6,7 @@ use AppendIterator;
 use BagOStuff;
 use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface;
 use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
+use MediaWiki\Rest\Validator\Validator;
 use Wikimedia\ObjectFactory;
 
 /**
@@ -44,6 +45,12 @@ class Router {
        /** @var BasicAuthorizerInterface */
        private $basicAuth;
 
+       /** @var ObjectFactory */
+       private $objectFactory;
+
+       /** @var Validator */
+       private $restValidator;
+
        /**
         * @param string[] $routeFiles List of names of JSON files containing routes
         * @param array $extraRoutes Extension route array
@@ -51,10 +58,13 @@ class Router {
         * @param BagOStuff $cacheBag A cache in which to store the matcher trees
         * @param ResponseFactory $responseFactory
         * @param BasicAuthorizerInterface $basicAuth
+        * @param ObjectFactory $objectFactory
+        * @param Validator $restValidator
         */
        public function __construct( $routeFiles, $extraRoutes, $rootPath,
                BagOStuff $cacheBag, ResponseFactory $responseFactory,
-               BasicAuthorizerInterface $basicAuth
+               BasicAuthorizerInterface $basicAuth, ObjectFactory $objectFactory,
+               Validator $restValidator
        ) {
                $this->routeFiles = $routeFiles;
                $this->extraRoutes = $extraRoutes;
@@ -62,6 +72,8 @@ class Router {
                $this->cacheBag = $cacheBag;
                $this->responseFactory = $responseFactory;
                $this->basicAuth = $basicAuth;
+               $this->objectFactory = $objectFactory;
+               $this->restValidator = $restValidator;
        }
 
        /**
@@ -245,9 +257,10 @@ class Router {
                $request->setPathParams( array_map( 'rawurldecode', $match['params'] ) );
                $spec = $match['userData'];
                $objectFactorySpec = array_intersect_key( $spec,
+                       // @todo ObjectFactory supports more keys than this.
                        [ 'factory' => true, 'class' => true, 'args' => true ] );
                /** @var $handler Handler (annotation for PHPStorm) */
-               $handler = ObjectFactory::getObjectFromSpec( $objectFactorySpec );
+               $handler = $this->objectFactory->createObject( $objectFactorySpec );
                $handler->init( $this, $request, $spec, $this->responseFactory );
 
                try {
@@ -268,6 +281,9 @@ class Router {
                if ( $authResult ) {
                        return $this->responseFactory->createHttpError( 403, [ 'error' => $authResult ] );
                }
+
+               $handler->validate( $this->restValidator );
+
                $response = $handler->execute();
                if ( !( $response instanceof ResponseInterface ) ) {
                        $response = $this->responseFactory->createFromReturnValue( $response );
index 3718d66..3c19e48 100644 (file)
@@ -14,7 +14,26 @@ namespace MediaWiki\Rest;
  */
 class SimpleHandler extends Handler {
        public function execute() {
-               $params = array_values( $this->getRequest()->getPathParams() );
+               $paramSettings = $this->getParamSettings();
+               $validatedParams = $this->getValidatedParams();
+               $unvalidatedParams = [];
+               $params = [];
+               foreach ( $this->getRequest()->getPathParams() as $name => $value ) {
+                       $source = $paramSettings[$name][self::PARAM_SOURCE] ?? 'unknown';
+                       if ( $source !== 'path' ) {
+                               $unvalidatedParams[] = $name;
+                               $params[] = $value;
+                       } else {
+                               $params[] = $validatedParams[$name];
+                       }
+               }
+
+               if ( $unvalidatedParams ) {
+                       throw new \LogicException(
+                               'Path parameters were not validated: ' . implode( ', ', $unvalidatedParams )
+                       );
+               }
+
                // @phan-suppress-next-line PhanUndeclaredMethod
                return $this->run( ...$params );
        }
diff --git a/includes/Rest/Validator/BodyValidator.php b/includes/Rest/Validator/BodyValidator.php
new file mode 100644 (file)
index 0000000..0147fa8
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+namespace MediaWiki\Rest\Validator;
+
+use MediaWiki\Rest\HttpException;
+use MediaWiki\Rest\RequestInterface;
+
+/**
+ * Interface for validating a request body
+ */
+interface BodyValidator {
+
+       /**
+        * 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
+        * @return mixed
+        * @throws HttpException on validation failure
+        */
+       public function validateBody( RequestInterface $request );
+
+}
diff --git a/includes/Rest/Validator/NullBodyValidator.php b/includes/Rest/Validator/NullBodyValidator.php
new file mode 100644 (file)
index 0000000..4fba5fb
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+namespace MediaWiki\Rest\Validator;
+
+use MediaWiki\Rest\RequestInterface;
+
+/**
+ * Do-nothing body validator
+ */
+class NullBodyValidator implements BodyValidator {
+
+       public function validateBody( RequestInterface $request ) {
+               return null;
+       }
+
+}
diff --git a/includes/Rest/Validator/ParamValidatorCallbacks.php b/includes/Rest/Validator/ParamValidatorCallbacks.php
new file mode 100644 (file)
index 0000000..6c54a50
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+namespace MediaWiki\Rest\Validator;
+
+use InvalidArgumentException;
+use MediaWiki\Rest\RequestInterface;
+use Psr\Http\Message\UploadedFileInterface;
+use User;
+use Wikimedia\ParamValidator\Callbacks;
+use Wikimedia\ParamValidator\ValidationException;
+
+class ParamValidatorCallbacks implements Callbacks {
+
+       /** @var RequestInterface */
+       private $request;
+
+       /** @var User */
+       private $user;
+
+       public function __construct( RequestInterface $request, User $user ) {
+               $this->request = $request;
+               $this->user = $user;
+       }
+
+       /**
+        * Get the raw parameters from a source in the request
+        * @param string $source 'path', 'query', or 'post'
+        * @return array
+        */
+       private function getParamsFromSource( $source ) {
+               switch ( $source ) {
+                       case 'path':
+                               return $this->request->getPathParams();
+
+                       case 'query':
+                               return $this->request->getQueryParams();
+
+                       case 'post':
+                               return $this->request->getPostParams();
+
+                       default:
+                               throw new InvalidArgumentException( __METHOD__ . ": Invalid source '$source'" );
+               }
+       }
+
+       public function hasParam( $name, array $options ) {
+               $params = $this->getParamsFromSource( $options['source'] );
+               return isset( $params[$name] );
+       }
+
+       public function getValue( $name, $default, array $options ) {
+               $params = $this->getParamsFromSource( $options['source'] );
+               return $params[$name] ?? $default;
+               // @todo Should normalization to NFC UTF-8 be done here (much like in the
+               // action API and the rest of MW), or should it be left to handlers to
+               // do whatever normalization they need?
+       }
+
+       public function hasUpload( $name, array $options ) {
+               if ( $options['source'] !== 'post' ) {
+                       return false;
+               }
+               return $this->getUploadedFile( $name, $options ) !== null;
+       }
+
+       public function getUploadedFile( $name, array $options ) {
+               if ( $options['source'] !== 'post' ) {
+                       return null;
+               }
+               $upload = $this->request->getUploadedFiles()[$name] ?? null;
+               return $upload instanceof UploadedFileInterface ? $upload : null;
+       }
+
+       public function recordCondition( ValidationException $condition, array $options ) {
+               // @todo Figure out how to handle warnings
+       }
+
+       public function useHighLimits( array $options ) {
+               return $this->user->isAllowed( 'apihighlimits' );
+       }
+
+}
diff --git a/includes/Rest/Validator/Validator.php b/includes/Rest/Validator/Validator.php
new file mode 100644 (file)
index 0000000..cee1cdb
--- /dev/null
@@ -0,0 +1,163 @@
+<?php
+
+namespace MediaWiki\Rest\Validator;
+
+use MediaWiki\Rest\Handler;
+use MediaWiki\Rest\HttpException;
+use MediaWiki\Rest\RequestInterface;
+use User;
+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 static $typeDefs = [
+               '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 static $noBodyMethods = [ 'GET', 'HEAD', 'DELETE' ];
+
+       /** @var string[] HTTP request methods that we expect always to have a payload */
+       private static $bodyMethods = [ 'POST', 'PUT' ];
+
+       /** @var string[] Content types handled via $_POST */
+       private static $formDataContentTypes = [
+               'application/x-www-form-urlencoded',
+               'multipart/form-data',
+       ];
+
+       /** @var ParamValidator */
+       private $paramValidator;
+
+       /**
+        * @internal
+        * @param ObjectFactory $objectFactory
+        * @param RequestInterface $request
+        * @param User $user
+        */
+       public function __construct(
+               ObjectFactory $objectFactory, RequestInterface $request, User $user
+       ) {
+               $this->paramValidator = new ParamValidator(
+                       new ParamValidatorCallbacks( $request, $user ),
+                       $objectFactory,
+                       [
+                               'typeDefs' => self::$typeDefs,
+                       ]
+               );
+       }
+
+       /**
+        * 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 HttpException( 'Parameter validation failed', 400, [
+                                       'error' => 'parameter-validation-failed',
+                                       'name' => $e->getParamName(),
+                                       'value' => $e->getParamValue(),
+                                       'failureCode' => $e->getFailureCode(),
+                                       'failureData' => $e->getFailureData(),
+                               ] );
+                       }
+               }
+               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::$noBodyMethods, 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::$bodyMethods, 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::$formDataContentTypes, true ) ) {
+                       return null;
+               }
+
+               // Validate the body. BodyValidator throws an HttpException on failure.
+               return $handler->getBodyValidator( $ct )->validateBody( $request );
+       }
+
+}
index 3c6573a..2d1fd98 100644 (file)
@@ -9,8 +9,11 @@ use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\RequestData;
 use MediaWiki\Rest\ResponseFactory;
 use MediaWiki\Rest\Router;
+use MediaWiki\Rest\Validator\Validator;
 use MediaWikiTestCase;
+use Psr\Container\ContainerInterface;
 use User;
+use Wikimedia\ObjectFactory;
 
 /**
  * @group Database
@@ -21,7 +24,7 @@ use User;
  * @covers \MediaWiki\Rest\BasicAccess\MWBasicRequestAuthorizer
  */
 class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
-       private function createRouter( $userRights ) {
+       private function createRouter( $userRights, $request ) {
                $user = User::newFromName( 'Test user' );
                // Don't allow the rights to everybody so that user rights kick in.
                $this->mergeMwGlobalArrayValue( 'wgGroupPermissions', [ '*' => $userRights ] );
@@ -34,18 +37,25 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
 
                global $IP;
 
+               $objectFactory = new ObjectFactory(
+                       $this->getMockForAbstractClass( ContainerInterface::class )
+               );
+
                return new Router(
                        [ "$IP/tests/phpunit/unit/includes/Rest/testRoutes.json" ],
                        [],
                        '/rest',
                        new \EmptyBagOStuff(),
                        new ResponseFactory(),
-                       new MWBasicAuthorizer( $user, MediaWikiServices::getInstance()->getPermissionManager() ) );
+                       new MWBasicAuthorizer( $user, MediaWikiServices::getInstance()->getPermissionManager() ),
+                       $objectFactory,
+                       new Validator( $objectFactory, $request, $user )
+               );
        }
 
        public function testReadDenied() {
-               $router = $this->createRouter( [ 'read' => false ] );
                $request = new RequestData( [ 'uri' => new Uri( '/rest/user/joe/hello' ) ] );
+               $router = $this->createRouter( [ 'read' => false ], $request );
                $response = $router->execute( $request );
                $this->assertSame( 403, $response->getStatusCode() );
 
@@ -56,8 +66,8 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
        }
 
        public function testReadAllowed() {
-               $router = $this->createRouter( [ 'read' => true ] );
                $request = new RequestData( [ 'uri' => new Uri( '/rest/user/joe/hello' ) ] );
+               $router = $this->createRouter( [ 'read' => true ], $request );
                $response = $router->execute( $request );
                $this->assertSame( 200, $response->getStatusCode() );
        }
@@ -75,10 +85,10 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
        }
 
        public function testWriteDenied() {
-               $router = $this->createRouter( [ 'read' => true, 'writeapi' => false ] );
                $request = new RequestData( [
                        'uri' => new Uri( '/rest/mock/MWBasicRequestAuthorizerTest/write' )
                ] );
+               $router = $this->createRouter( [ 'read' => true, 'writeapi' => false ], $request );
                $response = $router->execute( $request );
                $this->assertSame( 403, $response->getStatusCode() );
 
@@ -89,10 +99,10 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
        }
 
        public function testWriteAllowed() {
-               $router = $this->createRouter( [ 'read' => true, 'writeapi' => true ] );
                $request = new RequestData( [
                        'uri' => new Uri( '/rest/mock/MWBasicRequestAuthorizerTest/write' )
                ] );
+               $router = $this->createRouter( [ 'read' => true, 'writeapi' => true ], $request );
                $response = $router->execute( $request );
 
                $this->assertSame( 200, $response->getStatusCode() );
index b599e9d..b984895 100644 (file)
@@ -9,10 +9,15 @@ use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer;
 use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\EntryPoint;
 use MediaWiki\Rest\RequestData;
+use MediaWiki\Rest\RequestInterface;
 use MediaWiki\Rest\ResponseFactory;
 use MediaWiki\Rest\Router;
+use MediaWiki\Rest\Validator\Validator;
+use Psr\Container\ContainerInterface;
 use RequestContext;
 use WebResponse;
+use Wikimedia\ObjectFactory;
+use User;
 
 /**
  * @covers \MediaWiki\Rest\EntryPoint
@@ -21,16 +26,23 @@ use WebResponse;
 class EntryPointTest extends \MediaWikiTestCase {
        private static $mockHandler;
 
-       private function createRouter() {
+       private function createRouter( RequestInterface $request ) {
                global $IP;
 
+               $objectFactory = new ObjectFactory(
+                       $this->getMockForAbstractClass( ContainerInterface::class )
+               );
+
                return new Router(
                        [ "$IP/tests/phpunit/unit/includes/Rest/testRoutes.json" ],
                        [],
                        '/rest',
                        new EmptyBagOStuff(),
                        new ResponseFactory(),
-                       new StaticBasicAuthorizer() );
+                       new StaticBasicAuthorizer(),
+                       $objectFactory,
+                       new Validator( $objectFactory, $request, new User )
+               );
        }
 
        private function createWebResponse() {
@@ -58,11 +70,12 @@ class EntryPointTest extends \MediaWikiTestCase {
                                [ 'Foo: Bar', true, null ]
                        );
 
+               $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/header' ) ] );
                $entryPoint = new EntryPoint(
                        RequestContext::getMain(),
-                       new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/header' ) ] ),
+                       $request,
                        $webResponse,
-                       $this->createRouter() );
+                       $this->createRouter( $request ) );
                $entryPoint->execute();
                $this->assertTrue( true );
        }
@@ -83,11 +96,12 @@ class EntryPointTest extends \MediaWikiTestCase {
         * Make sure EntryPoint rewinds a seekable body stream before reading.
         */
        public function testBodyRewind() {
+               $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/bodyRewind' ) ] );
                $entryPoint = new EntryPoint(
                        RequestContext::getMain(),
-                       new RequestData( [ 'uri' => new Uri( '/rest/mock/EntryPoint/bodyRewind' ) ] ),
+                       $request,
                        $this->createWebResponse(),
-                       $this->createRouter() );
+                       $this->createRouter( $request ) );
                ob_start();
                $entryPoint->execute();
                $this->assertSame( 'hello', ob_get_clean() );
index 188629f..91652a2 100644 (file)
@@ -8,6 +8,10 @@ use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer;
 use MediaWiki\Rest\RequestData;
 use MediaWiki\Rest\ResponseFactory;
 use MediaWiki\Rest\Router;
+use MediaWiki\Rest\Validator\Validator;
+use Psr\Container\ContainerInterface;
+use Wikimedia\ObjectFactory;
+use User;
 
 /**
  * @covers \MediaWiki\Rest\Handler\HelloHandler
@@ -48,14 +52,21 @@ class HelloHandlerTest extends \MediaWikiUnitTestCase {
 
        /** @dataProvider provideTestViaRouter */
        public function testViaRouter( $requestInfo, $responseInfo ) {
+               $objectFactory = new ObjectFactory(
+                       $this->getMockForAbstractClass( ContainerInterface::class )
+               );
+
+               $request = new RequestData( $requestInfo );
                $router = new Router(
                        [ __DIR__ . '/../testRoutes.json' ],
                        [],
                        '/rest',
                        new EmptyBagOStuff(),
                        new ResponseFactory(),
-                       new StaticBasicAuthorizer() );
-               $request = new RequestData( $requestInfo );
+                       new StaticBasicAuthorizer(),
+                       $objectFactory,
+                       new Validator( $objectFactory, $request, new User )
+               );
                $response = $router->execute( $request );
                if ( isset( $responseInfo['statusCode'] ) ) {
                        $this->assertSame( $responseInfo['statusCode'], $response->getStatusCode() );
index cacccb9..e16ea25 100644 (file)
@@ -7,37 +7,48 @@ use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer;
 use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\HttpException;
 use MediaWiki\Rest\RequestData;
+use MediaWiki\Rest\RequestInterface;
 use MediaWiki\Rest\ResponseFactory;
 use MediaWiki\Rest\Router;
+use MediaWiki\Rest\Validator\Validator;
+use Psr\Container\ContainerInterface;
+use Wikimedia\ObjectFactory;
+use User;
 
 /**
  * @covers \MediaWiki\Rest\Router
  */
 class RouterTest extends \MediaWikiUnitTestCase {
        /** @return Router */
-       private function createRouter( $authError = null ) {
+       private function createRouter( RequestInterface $request, $authError = null ) {
+               $objectFactory = new ObjectFactory(
+                       $this->getMockForAbstractClass( ContainerInterface::class )
+               );
                return new Router(
                        [ __DIR__ . '/testRoutes.json' ],
                        [],
                        '/rest',
                        new \EmptyBagOStuff(),
                        new ResponseFactory(),
-                       new StaticBasicAuthorizer( $authError ) );
+                       new StaticBasicAuthorizer( $authError ),
+                       $objectFactory,
+                       new Validator( $objectFactory, $request, new User )
+               );
        }
 
        public function testPrefixMismatch() {
-               $router = $this->createRouter();
                $request = new RequestData( [ 'uri' => new Uri( '/bogus' ) ] );
+               $router = $this->createRouter( $request );
                $response = $router->execute( $request );
                $this->assertSame( 404, $response->getStatusCode() );
        }
 
        public function testWrongMethod() {
-               $router = $this->createRouter();
                $request = new RequestData( [
                        'uri' => new Uri( '/rest/user/joe/hello' ),
                        'method' => 'OPTIONS'
                ] );
+               $router = $this->createRouter( $request );
                $response = $router->execute( $request );
                $this->assertSame( 405, $response->getStatusCode() );
                $this->assertSame( 'Method Not Allowed', $response->getReasonPhrase() );
@@ -45,8 +56,8 @@ class RouterTest extends \MediaWikiUnitTestCase {
        }
 
        public function testNoMatch() {
-               $router = $this->createRouter();
                $request = new RequestData( [ 'uri' => new Uri( '/rest/bogus' ) ] );
+               $router = $this->createRouter( $request );
                $response = $router->execute( $request );
                $this->assertSame( 404, $response->getStatusCode() );
                // TODO: add more information to the response body and test for its presence here
@@ -61,8 +72,8 @@ class RouterTest extends \MediaWikiUnitTestCase {
        }
 
        public function testException() {
-               $router = $this->createRouter();
                $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/RouterTest/throw' ) ] );
+               $router = $this->createRouter( $request );
                $response = $router->execute( $request );
                $this->assertSame( 555, $response->getStatusCode() );
                $body = $response->getBody();
@@ -72,9 +83,9 @@ class RouterTest extends \MediaWikiUnitTestCase {
        }
 
        public function testBasicAccess() {
-               $router = $this->createRouter( 'test-error' );
                // Using the throwing handler is a way to assert that the handler is not executed
                $request = new RequestData( [ 'uri' => new Uri( '/rest/mock/RouterTest/throw' ) ] );
+               $router = $this->createRouter( $request, 'test-error' );
                $response = $router->execute( $request );
                $this->assertSame( 403, $response->getStatusCode() );
                $body = $response->getBody();