diff --git a/src/Middleware/RequestQueryValidationMiddleware.php b/src/Middleware/RequestQueryValidationMiddleware.php new file mode 100644 index 0000000..e1b9c48 --- /dev/null +++ b/src/Middleware/RequestQueryValidationMiddleware.php @@ -0,0 +1,111 @@ + + * @copyright Copyright (c) 2019, Anatoly Fenric + * @license https://github.com/sunrise-php/http-router-openapi/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router-openapi + */ + +namespace Sunrise\Http\Router\OpenApi\Middleware; + +/** + * Import classes + */ +use JsonSchema\Validator; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\Exception\BadRequestException; +use Sunrise\Http\Router\OpenApi\Utility\JsonSchemaBuilder; +use Sunrise\Http\Router\Route; +use Sunrise\Http\Router\RouteInterface; +use ReflectionClass; +use RuntimeException; + +/** + * Import functions + */ +use function class_exists; +use function json_decode; +use function json_encode; + +/** + * RequestQueryValidationMiddleware + * + * Don't use this middleware globally! + */ +class RequestQueryValidationMiddleware implements MiddlewareInterface +{ + + /** + * Constructor of the class + * + * @throws RuntimeException + * + * @codeCoverageIgnore + */ + public function __construct() + { + if (!class_exists('JsonSchema\Validator')) { + throw new RuntimeException('To use request body validation, install the "justinrainbow/json-schema"'); + } + } + + /** + * {@inheritDoc} + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + $this->validate($request); + + return $handler->handle($request); + } + + /** + * Validates the given request + * + * @param ServerRequestInterface $request + * + * @return void + * + * @throws BadRequestException + */ + protected function validate(ServerRequestInterface $request) : void + { + $route = $request->getAttribute(Route::ATTR_NAME_FOR_ROUTE); + + if (!($route instanceof RouteInterface)) { + return; + } + + $operationSource = new ReflectionClass($route->getRequestHandler()); + $jsonSchemaBuilder = new JsonSchemaBuilder($operationSource); + $jsonSchema = $jsonSchemaBuilder->forRequestQueryParams(); + + if (null === $jsonSchema) { + return; + } + + $payload = json_encode($request->getQueryParams()); + $payload = (object) json_decode($payload); + + $validator = new Validator(); + $validator->validate($payload, $jsonSchema); + + if (!$validator->isValid()) { + throw new BadRequestException('The request query parameters is not valid for this resource.', [ + 'jsonSchema' => $jsonSchema, + 'violations' => $validator->getErrors(), + ]); + } + } +} diff --git a/src/Utility/JsonSchemaBuilder.php b/src/Utility/JsonSchemaBuilder.php index 598acec..6432aa1 100644 --- a/src/Utility/JsonSchemaBuilder.php +++ b/src/Utility/JsonSchemaBuilder.php @@ -16,6 +16,7 @@ */ use Doctrine\Common\Annotations\SimpleAnnotationReader; use Sunrise\Http\Router\OpenApi\Annotation\OpenApi\Operation; +use Sunrise\Http\Router\OpenApi\Annotation\OpenApi\ParameterReference; use Sunrise\Http\Router\OpenApi\Annotation\OpenApi\RequestBodyReference; use Sunrise\Http\Router\OpenApi\Annotation\OpenApi\ResponseReference; use Sunrise\Http\Router\OpenApi\Exception\UnsupportedMediaTypeException; @@ -26,6 +27,7 @@ * Import functions */ use function array_keys; +use function array_walk; use function array_walk_recursive; use function str_replace; @@ -65,6 +67,60 @@ public function __construct(ReflectionClass $operationSource) $this->annotationReader->addNamespace(OpenApi::ANNOTATIONS_NAMESPACE); } + /** + * Builds a JSON schema for a request query parameters + * + * @return null|array + */ + public function forRequestQueryParams() : ?array + { + $operation = $this->annotationReader->getClassAnnotation($this->operationSource, Operation::class); + if (empty($operation->parameters)) { + return null; + } + + $jsonSchema = $this->jsonSchemaBlank; + $jsonSchema['type'] = 'object'; + $jsonSchema['required'] = []; + $jsonSchema['properties'] = []; + $jsonSchema['definitions'] = []; + + foreach ($operation->parameters as $parameter) { + if ($parameter instanceof ParameterReference) { + $parameter = $parameter->getAnnotation($this->annotationReader); + } + + if (!('query' === $parameter->in)) { + continue; + } + + if ($parameter->required) { + $jsonSchema['required'][] = $parameter->name; + } + + if ($parameter->schema) { + $jsonSchema['properties'][$parameter->name] = $parameter->schema; + } + } + + if (empty($jsonSchema['required']) && empty($jsonSchema['properties'])) { + return null; + } + + $referencedObjects = $operation->getReferencedObjects($this->annotationReader); + foreach ($referencedObjects as $referencedObject) { + if ('schemas' === $referencedObject->getComponentName()) { + $jsonSchema['definitions'][$referencedObject->getReferenceName()] = $referencedObject->toArray(); + } + } + + array_walk($jsonSchema['properties'], function (&$schema) { + $schema = $schema->toArray(); + }); + + return $this->fixReferences($jsonSchema); + } + /** * Builds a JSON schema for a request body * diff --git a/tests/Utility/JsonSchemaBuilderTest.php b/tests/Utility/JsonSchemaBuilderTest.php index 7091951..338c92d 100644 --- a/tests/Utility/JsonSchemaBuilderTest.php +++ b/tests/Utility/JsonSchemaBuilderTest.php @@ -77,6 +77,86 @@ class JsonSchemaBuilderTest extends TestCase */ private $bar; + /** + * @OpenApi\Schema( + * refName="ReferencedBazProperty", + * type="string", + * ) + */ + private $baz; + + /** + * @return void + */ + public function testBuildJsonSchemaForRequestQuery() : void + { + /** + * @OpenApi\Operation( + * parameters={ + * @OpenApi\Parameter( + * in="cookie", + * name="foo", + * schema=@OpenApi\Schema( + * type="string", + * ), + * ), + * @OpenApi\Parameter( + * in="query", + * name="bar", + * schema=@OpenApi\Schema( + * type="string", + * ), + * ), + * @OpenApi\Parameter( + * in="query", + * name="baz", + * schema=@OpenApi\SchemaReference( + * class="Sunrise\Http\Router\OpenApi\Tests\Utility\JsonSchemaBuilderTest", + * property="baz", + * ), + * ), + * }, + * responses={ + * 200: @OpenApi\Response( + * description="OK", + * ), + * }, + * ) + */ + $class = new class + { + }; + + $classReflection = new ReflectionClass($class); + $jsonSchemaBuilder = new JsonSchemaBuilder($classReflection); + $jsonSchema = $jsonSchemaBuilder->forRequestQueryParams(); + + $this->assertSame([ + '$schema' => 'http://json-schema.org/draft-00/schema#', + 'type' => 'object', + 'required' => [], + 'properties' => [ + 'bar' => [ + 'type' => 'string', + ], + 'baz' => [ + '$ref' => '#/definitions/ReferencedBazProperty', + ], + ], + 'definitions' => [ + 'ReferencedBazProperty' => [ + 'type' => 'string', + ], + ], + ], $jsonSchema); + + $classReflection = new ReflectionClass(new \stdClass); + $jsonSchemaBuilder = new JsonSchemaBuilder($classReflection); + $jsonSchema = $jsonSchemaBuilder->forRequestQueryParams(); + + $this->assertNull($jsonSchema); + } + /** * @return void */