Commit fd47b158 authored by Oliver Bartsch's avatar Oliver Bartsch
Browse files

Replace RequiredValidator with checks in RouteResolver

parent a3ef3bdf
...@@ -84,7 +84,7 @@ abstract class AbstractApiController implements RequestHandlerInterface ...@@ -84,7 +84,7 @@ abstract class AbstractApiController implements RequestHandlerInterface
} }
} }
if (!$routeArgument->isValid()) { if (!$routeArgument->isValid()) {
$invalidArguments[] = $routeArgument; $invalidArguments[$routeArgument->getName()] = $routeArgument;
} }
} }
......
<?php
declare(strict_types = 1);
namespace T3o\Ter\Exception;
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
class RequiredArgumentMissingException extends Exception
{
}
...@@ -71,6 +71,7 @@ final class RestRouteDispatcher implements MiddlewareInterface ...@@ -71,6 +71,7 @@ final class RestRouteDispatcher implements MiddlewareInterface
$this->routeConfiguration $this->routeConfiguration
->setBase(self::API_REQUEST_PATH . $version) ->setBase(self::API_REQUEST_PATH . $version)
->setPath(str_replace(self::API_REQUEST_PATH . $version, '', $path))
->setSchema($schema) ->setSchema($schema)
->createRouteCollection(); ->createRouteCollection();
......
...@@ -12,7 +12,6 @@ namespace T3o\Ter\Rest\RouteArgument; ...@@ -12,7 +12,6 @@ namespace T3o\Ter\Rest\RouteArgument;
* of the License, or any later version. * of the License, or any later version.
*/ */
use T3o\Ter\Rest\Validaton\RequiredValidator;
use T3o\Ter\Rest\Validaton\ValidationErrorInterface; use T3o\Ter\Rest\Validaton\ValidationErrorInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\GeneralUtility;
...@@ -30,10 +29,6 @@ abstract class AbstractRouteArgument implements RouteArgumentInterface, \JsonSer ...@@ -30,10 +29,6 @@ abstract class AbstractRouteArgument implements RouteArgumentInterface, \JsonSer
{ {
$this->name = $name; $this->name = $name;
$this->configuration = $configuration; $this->configuration = $configuration;
if ((bool)($this->configuration['required'] ?? false)) {
$this->validators[RequiredValidator::class] = [];
}
} }
public function getName(): string public function getName(): string
......
...@@ -15,6 +15,7 @@ namespace T3o\Ter\Rest\RouteArgument; ...@@ -15,6 +15,7 @@ namespace T3o\Ter\Rest\RouteArgument;
use T3o\Ter\Rest\Validaton\EnumValidator; use T3o\Ter\Rest\Validaton\EnumValidator;
use T3o\Ter\Rest\Validaton\MaxValidator; use T3o\Ter\Rest\Validaton\MaxValidator;
use T3o\Ter\Rest\Validaton\MinValidator; use T3o\Ter\Rest\Validaton\MinValidator;
use TYPO3\CMS\Core\Utility\MathUtility;
/** /**
* Implementation for an integer argument * Implementation for an integer argument
...@@ -23,10 +24,18 @@ class IntegerArgument extends AbstractRouteArgument ...@@ -23,10 +24,18 @@ class IntegerArgument extends AbstractRouteArgument
{ {
protected int $value; protected int $value;
public function __construct(string $name, array $configuration, int $value) public function __construct(string $name, array $configuration, $value)
{ {
if (!is_int($value) && !MathUtility::canBeInterpretedAsInteger($value)) {
throw new \InvalidArgumentException(
sprintf('Value \'%s\' for argument \'%s\' must be of type integer.', $value, $name),
1601313518
);
}
parent::__construct($name, $configuration); parent::__construct($name, $configuration);
$this->value = $value;
$this->value = (int)$value;
$this->addValidatorsFromConfiguration(); $this->addValidatorsFromConfiguration();
} }
......
...@@ -23,9 +23,17 @@ class ObjectArgument extends AbstractRouteArgument implements DeepObjectRouteArg ...@@ -23,9 +23,17 @@ class ObjectArgument extends AbstractRouteArgument implements DeepObjectRouteArg
protected array $value; protected array $value;
protected array $properties; protected array $properties;
public function __construct(string $name, array $configuration, array $value) public function __construct(string $name, array $configuration, $value)
{ {
if (!is_array($value)) {
throw new \InvalidArgumentException(
sprintf('Value \'%s\' for argument \'%s\' must be of type array.', $value, $name),
1601313412
);
}
parent::__construct($name, $configuration); parent::__construct($name, $configuration);
$this->value = $value; $this->value = $value;
$routeArgumentFactory = GeneralUtility::makeInstance( $routeArgumentFactory = GeneralUtility::makeInstance(
RouteArgumentFactory::class, RouteArgumentFactory::class,
......
...@@ -29,7 +29,7 @@ final class RouteArgumentFactory ...@@ -29,7 +29,7 @@ final class RouteArgumentFactory
public function create(string $name, array $configuration, $value): RouteArgumentInterface public function create(string $name, array $configuration, $value): RouteArgumentInterface
{ {
$type = ucfirst($configuration['schema']['type'] ?? ''); $type = ucfirst((string)($configuration['schema']['type'] ?? ''));
if ($type === '') { if ($type === '') {
throw new \InvalidArgumentException(sprintf('RouteArgument type cannot be empty for argument: %s', $name), 1601024733); throw new \InvalidArgumentException(sprintf('RouteArgument type cannot be empty for argument: %s', $name), 1601024733);
......
...@@ -24,9 +24,17 @@ class StringArgument extends AbstractRouteArgument ...@@ -24,9 +24,17 @@ class StringArgument extends AbstractRouteArgument
{ {
protected string $value; protected string $value;
public function __construct(string $name, array $configuration, string $value) public function __construct(string $name, array $configuration, $value)
{ {
if (!is_string($value)) {
throw new \InvalidArgumentException(
sprintf('Value \'%s\' for argument \'%s\' must be of type integer.', $value, $name),
1601313573
);
}
parent::__construct($name, $configuration); parent::__construct($name, $configuration);
$this->value = $value; $this->value = $value;
$this->addValidatorsFromConfiguration(); $this->addValidatorsFromConfiguration();
} }
......
...@@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; ...@@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
final class RouteConfiguration implements SingletonInterface final class RouteConfiguration implements SingletonInterface
{ {
protected string $base; protected string $base;
protected string $path;
protected array $schema; protected array $schema;
protected RouteCollection $routeCollection; protected RouteCollection $routeCollection;
...@@ -42,6 +43,17 @@ final class RouteConfiguration implements SingletonInterface ...@@ -42,6 +43,17 @@ final class RouteConfiguration implements SingletonInterface
return $this->base; return $this->base;
} }
public function setPath(string $path): self
{
$this->path = $path;
return $this;
}
public function getPath(): string
{
return $this->path;
}
public function setSchema(array $schema): self public function setSchema(array $schema): self
{ {
$this->schema = $schema; $this->schema = $schema;
...@@ -53,6 +65,11 @@ final class RouteConfiguration implements SingletonInterface ...@@ -53,6 +65,11 @@ final class RouteConfiguration implements SingletonInterface
return $this->schema; return $this->schema;
} }
public function getRoute(string $operationId): ?Route
{
return $this->routeCollection->get($operationId);
}
public function getRouteCollection(): RouteCollection public function getRouteCollection(): RouteCollection
{ {
return $this->routeCollection; return $this->routeCollection;
...@@ -90,13 +107,40 @@ final class RouteConfiguration implements SingletonInterface ...@@ -90,13 +107,40 @@ final class RouteConfiguration implements SingletonInterface
return $this; return $this;
} }
public function getEndpoint(string $fullPath): string public function getEndpointConfiguration(string $operationId): array
{ {
return '/' . ltrim(str_replace($this->base, '', $fullPath), '/'); $route = $this->getRoute($operationId);
if ($route === null) {
return [];
}
$path = $route->getPath();
$method = strtolower($route->getMethods()[0] ?? '');
if (isset($this->schema['paths'][$path][$method])) {
return (array)$this->schema['paths'][$path][$method];
}
return (array)($this->schema['paths'][$path] ?? []);
} }
public function getParameterConfiguration(string $name): array public function getParameterConfiguration(string $name): array
{ {
return (array)($this->schema['components']['parameters'][GeneralUtility::underscoredToUpperCamelCase($name)] ?? []); return (array)($this->schema['components']['parameters'][GeneralUtility::underscoredToUpperCamelCase($name)] ?? []);
} }
public function getParameterConfigurationByReference(string $reference): array
{
$parts = explode('/', str_replace('#/', '', $reference));
$configuration = $this->schema;
foreach ($parts as $part) {
if (isset($configuration[$part]) && is_array($configuration[$part])) {
$configuration = $configuration[$part];
}
}
return array_merge($configuration, ['reference' => end($parts)]);
}
} }
...@@ -17,6 +17,7 @@ use Psr\Http\Message\ServerRequestInterface; ...@@ -17,6 +17,7 @@ use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use T3o\Ter\Exception\RequiredArgumentMissingException;
use T3o\Ter\Rest\Response\ApiResponseFactory; use T3o\Ter\Rest\Response\ApiResponseFactory;
/** /**
...@@ -55,6 +56,8 @@ final class RouteHandler implements RequestHandlerInterface ...@@ -55,6 +56,8 @@ final class RouteHandler implements RequestHandlerInterface
} catch (MethodNotAllowedException $e) { } catch (MethodNotAllowedException $e) {
$message = 'The method ' . $request->getMethod() . ' is not allowed.'; $message = 'The method ' . $request->getMethod() . ' is not allowed.';
return $this->apiResponseFactory->createErrorResponseForRequest($request, 1600994273, $message, 405); return $this->apiResponseFactory->createErrorResponseForRequest($request, 1600994273, $message, 405);
} catch (RequiredArgumentMissingException $e) {
return $this->apiResponseFactory->createErrorResponseForRequest($request, 1600996241, $e->getMessage(), 400);
} catch (ResourceNotFoundException|\OutOfRangeException|\InvalidArgumentException $e) { } catch (ResourceNotFoundException|\OutOfRangeException|\InvalidArgumentException $e) {
return $this->apiResponseFactory->createErrorResponseForRequest($request, 1601289397, $e->getMessage()); return $this->apiResponseFactory->createErrorResponseForRequest($request, 1601289397, $e->getMessage());
} }
......
...@@ -16,6 +16,7 @@ use Psr\Http\Message\ServerRequestInterface; ...@@ -16,6 +16,7 @@ use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RequestContext;
use T3o\Ter\Exception\RequiredArgumentMissingException;
use T3o\Ter\Rest\RouteArgument\RouteArgumentFactory; use T3o\Ter\Rest\RouteArgument\RouteArgumentFactory;
use TYPO3\CMS\Core\Routing\RouteResultInterface; use TYPO3\CMS\Core\Routing\RouteResultInterface;
...@@ -46,7 +47,7 @@ final class RouteResolver ...@@ -46,7 +47,7 @@ final class RouteResolver
$resultParameters = (new UrlMatcher( $resultParameters = (new UrlMatcher(
$this->routeConfiguration->getRouteCollection(), $this->routeConfiguration->getRouteCollection(),
$this->getRequestContext($request) $this->getRequestContext($request)
))->match($this->routeConfiguration->getEndpoint($request->getUri()->getPath())); ))->match($this->routeConfiguration->getPath());
if (!($resultParameters['_route'] ?? false)) { if (!($resultParameters['_route'] ?? false)) {
throw new ResourceNotFoundException('No resource found for the given route.', 1600994133); throw new ResourceNotFoundException('No resource found for the given route.', 1600994133);
...@@ -63,7 +64,7 @@ final class RouteResolver ...@@ -63,7 +64,7 @@ final class RouteResolver
); );
$arguments = array_merge( $arguments = array_merge(
array_filter($resultParameters, static function ($_, $k){ array_filter($resultParameters, static function ($_, $k) {
return strpos($k, '_') !== 0; return strpos($k, '_') !== 0;
}, ARRAY_FILTER_USE_BOTH), }, ARRAY_FILTER_USE_BOTH),
$request->getQueryParams() $request->getQueryParams()
...@@ -76,6 +77,40 @@ final class RouteResolver ...@@ -76,6 +77,40 @@ final class RouteResolver
} }
} }
$endpointConfiguration = $this->routeConfiguration->getEndpointConfiguration(
$routeResultArguments->offsetGet('operationId')
);
if (isset($endpointConfiguration['parameters']) && is_array($endpointConfiguration['parameters'])) {
foreach ($endpointConfiguration['parameters'] as $parameter) {
if ((bool)($parameter['required'] ?? false)
&& !$routeResultArguments->hasRouteArgument($parameter['name'])
) {
throw new RequiredArgumentMissingException(
sprintf('%s argument %s is required but is missing in the request.',
ucfirst($parameter['in']),
$parameter['name']
),
1601369655
);
}
}
}
if (isset($endpointConfiguration['requestBody']) && is_array($endpointConfiguration['requestBody'])) {
foreach ($endpointConfiguration['requestBody'] as $key => $requestBody) {
if ($key === '$ref') {
$requestBody = $this->routeConfiguration->getParameterConfigurationByReference($requestBody);
}
if ((bool)($requestBody['required'] ?? false) && $request->getParsedBody() === null) {
throw new RequiredArgumentMissingException(
sprintf('Required request body for %s is missing in the request.', $requestBody['reference']),
1601369655
);
}
}
}
return $routeResultArguments; return $routeResultArguments;
} }
......
...@@ -74,7 +74,7 @@ final class RouteResultArguments implements RouteResultInterface ...@@ -74,7 +74,7 @@ final class RouteResultArguments implements RouteResultInterface
public function hasRouteArgument(string $name): bool public function hasRouteArgument(string $name): bool
{ {
return $this->routeArguments[$name] ?? false; return isset($this->routeArguments[$name]);
} }
public function getRouteArgument(string $name): RouteArgumentInterface public function getRouteArgument(string $name): RouteArgumentInterface
......
<?php
declare(strict_types = 1);
namespace T3o\Ter\Rest\Validaton;
/*
* This file is part of TYPO3 CMS-extension "ter", created by Oliver Bartsch.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*/
use T3o\Ter\Rest\RouteArgument\IntegerArgument;
use T3o\Ter\Rest\RouteArgument\RouteArgumentInterface;
use T3o\Ter\Rest\RouteArgument\StringArgument;
/**
* Validator implementation for OpenApi `required` property.
*
* @todo This validator may be superfluous as required arguments should be checked
* while resolving the route. Furthermore path arguments are already checked
* on url matching.
*/
final class RequiredValidator implements ValidatorInterface
{
public function validate(RouteArgumentInterface $routeArgument): void
{
$class = get_class($routeArgument);
switch ($class) {
case StringArgument::class:
$result = $routeArgument->getValue() !== '';
break;
case IntegerArgument::class:
// @todo what about negative values?
$result = $routeArgument->getValue() > 0;
break;
default:
throw new \InvalidArgumentException(__CLASS__ . ' can\'t handle ' . $class);
}
if (!$result) {
$routeArgument->addValidationError(
new ValidationError(
sprintf(
'The given value \'%s\' for argument \'%s\' must not be empty.',
$routeArgument->getValue(),
$routeArgument->getName()
)
)
);
}
}
}
...@@ -265,11 +265,13 @@ ...@@ -265,11 +265,13 @@
"type": "array", "type": "array",
"example": [ "example": [
{ {
"name": "argument", "some_arguemnt": {
"value": 100, "name": "some_argument",
"validationErrors": [ "value": 100,
"The given value 100 for argument must be at max 50" "validationErrors": [
] "The given value 100 for some_argument must be at max 50"
]
}
} }
], ],
"items": { "items": {
...@@ -444,11 +446,13 @@ ...@@ -444,11 +446,13 @@
"type": "array", "type": "array",
"example": [ "example": [
{ {
"name": "argument", "some_arguemnt": {
"value": 100, "name": "some_argument",
"validationErrors": [ "value": 100,
"The given value 100 for argument must be at max 50" "validationErrors": [
] "The given value 100 for some_argument must be at max 50"
]
}
} }
], ],
"items": { "items": {
...@@ -634,11 +638,13 @@ ...@@ -634,11 +638,13 @@
"type": "array", "type": "array",
"example": [ "example": [
{ {
"name": "argument", "some_arguemnt": {
"value": 100, "name": "some_argument",
"validationErrors": [ "value": 100,
"The given value 100 for argument must be at max 50" "validationErrors": [
] "The given value 100 for some_argument must be at max 50"
]
}
} }
], ],
"items": { "items": {
...@@ -829,11 +835,13 @@ ...@@ -829,11 +835,13 @@
"type": "array", "type": "array",
"example": [ "example": [
{ {
"name": "argument", "some_arguemnt": {
"value": 100, "name": "some_argument",
"validationErrors": [ "value": 100,
"The given value 100 for argument must be at max 50" "validationErrors": [
] "The given value 100 for some_argument must be at max 50"
]
}
} }
], ],
"items": { "items": {
...@@ -1009,11 +1017,13 @@ ...@@ -1009,11 +1017,13 @@
"type": "array", "type": "array",
"example": [ "example": [
{ {
"name": "argument", "some_arguemnt": {
"value": 100, "name": "some_argument",
"validationErrors": [ "value": 100,
"The given value 100 for argument must be at max 50" "validationErrors": [
] "The given value 100 for some_argument must be at max 50"
]
}
} }
], ],
"items": { "items": {
...@@ -1208,11 +1218,13 @@ ...@@ -1208,11 +1218,13 @@
"type": "array", "type": "array",
"example": [ "example": [
{ {
"name": "argument", "some_arguemnt": {
"value": 100, "name": "some_argument",
"validationErrors": [