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
}
}
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
$this->routeConfiguration
->setBase(self::API_REQUEST_PATH . $version)
->setPath(str_replace(self::API_REQUEST_PATH . $version, '', $path))
->setSchema($schema)
->createRouteCollection();
......
......@@ -12,7 +12,6 @@ namespace T3o\Ter\Rest\RouteArgument;
* of the License, or any later version.
*/
use T3o\Ter\Rest\Validaton\RequiredValidator;
use T3o\Ter\Rest\Validaton\ValidationErrorInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -30,10 +29,6 @@ abstract class AbstractRouteArgument implements RouteArgumentInterface, \JsonSer
{
$this->name = $name;
$this->configuration = $configuration;
if ((bool)($this->configuration['required'] ?? false)) {
$this->validators[RequiredValidator::class] = [];
}
}
public function getName(): string
......
......@@ -15,6 +15,7 @@ namespace T3o\Ter\Rest\RouteArgument;
use T3o\Ter\Rest\Validaton\EnumValidator;
use T3o\Ter\Rest\Validaton\MaxValidator;
use T3o\Ter\Rest\Validaton\MinValidator;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Implementation for an integer argument
......@@ -23,10 +24,18 @@ class IntegerArgument extends AbstractRouteArgument
{
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);
$this->value = $value;
$this->value = (int)$value;
$this->addValidatorsFromConfiguration();
}
......
......@@ -23,9 +23,17 @@ class ObjectArgument extends AbstractRouteArgument implements DeepObjectRouteArg
protected array $value;
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);
$this->value = $value;
$routeArgumentFactory = GeneralUtility::makeInstance(
RouteArgumentFactory::class,
......
......@@ -29,7 +29,7 @@ final class RouteArgumentFactory
public function create(string $name, array $configuration, $value): RouteArgumentInterface
{
$type = ucfirst($configuration['schema']['type'] ?? '');
$type = ucfirst((string)($configuration['schema']['type'] ?? ''));
if ($type === '') {
throw new \InvalidArgumentException(sprintf('RouteArgument type cannot be empty for argument: %s', $name), 1601024733);
......
......@@ -24,9 +24,17 @@ class StringArgument extends AbstractRouteArgument
{
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);
$this->value = $value;
$this->addValidatorsFromConfiguration();
}
......
......@@ -23,6 +23,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
final class RouteConfiguration implements SingletonInterface
{
protected string $base;
protected string $path;
protected array $schema;
protected RouteCollection $routeCollection;
......@@ -42,6 +43,17 @@ final class RouteConfiguration implements SingletonInterface
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
{
$this->schema = $schema;
......@@ -53,6 +65,11 @@ final class RouteConfiguration implements SingletonInterface
return $this->schema;
}
public function getRoute(string $operationId): ?Route
{
return $this->routeCollection->get($operationId);
}
public function getRouteCollection(): RouteCollection
{
return $this->routeCollection;
......@@ -90,13 +107,40 @@ final class RouteConfiguration implements SingletonInterface
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
{
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;
use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use T3o\Ter\Exception\RequiredArgumentMissingException;
use T3o\Ter\Rest\Response\ApiResponseFactory;
/**
......@@ -55,6 +56,8 @@ final class RouteHandler implements RequestHandlerInterface
} catch (MethodNotAllowedException $e) {
$message = 'The method ' . $request->getMethod() . ' is not allowed.';
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) {
return $this->apiResponseFactory->createErrorResponseForRequest($request, 1601289397, $e->getMessage());
}
......
......@@ -16,6 +16,7 @@ use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use T3o\Ter\Exception\RequiredArgumentMissingException;
use T3o\Ter\Rest\RouteArgument\RouteArgumentFactory;
use TYPO3\CMS\Core\Routing\RouteResultInterface;
......@@ -46,7 +47,7 @@ final class RouteResolver
$resultParameters = (new UrlMatcher(
$this->routeConfiguration->getRouteCollection(),
$this->getRequestContext($request)
))->match($this->routeConfiguration->getEndpoint($request->getUri()->getPath()));
))->match($this->routeConfiguration->getPath());
if (!($resultParameters['_route'] ?? false)) {
throw new ResourceNotFoundException('No resource found for the given route.', 1600994133);
......@@ -63,7 +64,7 @@ final class RouteResolver
);
$arguments = array_merge(
array_filter($resultParameters, static function ($_, $k){
array_filter($resultParameters, static function ($_, $k) {
return strpos($k, '_') !== 0;
}, ARRAY_FILTER_USE_BOTH),
$request->getQueryParams()
......@@ -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;
}
......
......@@ -74,7 +74,7 @@ final class RouteResultArguments implements RouteResultInterface
public function hasRouteArgument(string $name): bool
{
return $this->routeArguments[$name] ?? false;
return isset($this->routeArguments[$name]);
}
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 @@
"type": "array",
"example": [
{
"name": "argument",
"value": 100,
"validationErrors": [
"The given value 100 for argument must be at max 50"
]
"some_arguemnt": {
"name": "some_argument",
"value": 100,
"validationErrors": [
"The given value 100 for some_argument must be at max 50"
]
}
}
],
"items": {
......@@ -444,11 +446,13 @@
"type": "array",
"example": [
{
"name": "argument",
"value": 100,
"validationErrors": [
"The given value 100 for argument must be at max 50"
]
"some_arguemnt": {
"name": "some_argument",
"value": 100,
"validationErrors": [
"The given value 100 for some_argument must be at max 50"
]
}
}
],
"items": {
......@@ -634,11 +638,13 @@
"type": "array",
"example": [
{
"name": "argument",
"value": 100,
"validationErrors": [
"The given value 100 for argument must be at max 50"
]
"some_arguemnt": {
"name": "some_argument",
"value": 100,
"validationErrors": [
"The given value 100 for some_argument must be at max 50"
]
}
}
],
"items": {
......@@ -829,11 +835,13 @@
"type": "array",
"example": [
{
"name": "argument",
"value": 100,
"validationErrors": [
"The given value 100 for argument must be at max 50"
]
"some_arguemnt": {
"name": "some_argument",
"value": 100,
"validationErrors": [
"The given value 100 for some_argument must be at max 50"
]
}
}
],
"items": {
......@@ -1009,11 +1017,13 @@
"type": "array",
"example": [
{
"name": "argument",
"value": 100,
"validationErrors": [
"The given value 100 for argument must be at max 50"
]
"some_arguemnt": {
"name": "some_argument",
"value": 100,
"validationErrors": [
"The given value 100 for some_argument must be at max 50"
]
}
}
],
"items": {
......@@ -1208,11 +1218,13 @@
"type": "array",
"example": [
{
"name": "argument",
"value": 100,
"validationErrors": [
"The given value 100 for argument must be at max 50"
]
"some_arguemnt": {
"name": "some_argument",
"value": 100,
"validationErrors": [
"The given value 100 for some_argument must be at max 50"
]
}
}
],
"items": {
......@@ -1410,11 +1422,13 @@
"type": "array",
"example": [
{
"name": "argument",
"value": 100,
"validationErrors": [
"The given value 100 for argument must be at max 50"
]
"some_arguemnt": {
"name": "some_argument",
"value": 100,
"validationErrors": [
"The given value 100 for some_argument must be at max 50"
]
}
}
],
"items": {
......@@ -1625,11 +1639,13 @@
"type": "array",
"example": [
{
"name": "argument",
"value": 100,
"validationErrors": [
"The given value 100 for argument must be at max 50"
]
"some_arguemnt": {
"name": "some_argument",
"value": 100,
"validationErrors": [
"The given value 100 for some_argument must be at max 50"
]
}
}
],
"items": {
......@@ -1840,11 +1856,13 @@
"type": "array",
"example": [
{
"name": "argument",
"value": 100,
"validationErrors": [
"The given value 100 for argument must be at max 50"
]
"some_arguemnt": {
"name": "some_argument",
"value": 100,
"validationErrors": [
"The given value 100 for some_argument must be at max 50"
]
}
}
],
"items": {
......@@ -2056,11 +2074,13 @@
"type": "array",
"example": [
{
"name": "argument",
"value": 100,
"validationErrors": [
"The given value 100 for argument must be at max 50"
]
"some_arguemnt": {
"name": "some_argument",
"value": 100,
"validationErrors": [
"The given value 100 for some_argument must be at max 50"
]
}
}
],
"items": {
......@@ -2287,11 +2307,13 @@
"type": "array",
"example": [
{
"name": "argument",
"value": 100,
"validationErrors": [
"The given value 100 for argument must be at max 50"
]
"some_arguemnt": {
"name": "some_argument",
"value": 100,
"validationErrors": [
"The given value 100 for some_argument must be at max 50"
]
}
}
],
"items": {
......@@ -3464,11 +3486,13 @@
"type": "array",
"example": [
{
"name": "argument",
"value": 100,
"validationErrors": [
"The given value 100 for argument must be at max 50"
]
"some_arguemnt": {
"name": "some_argument",
"value": 100,
"validationErrors": [
"The given value 100 for some_argument must be at max 50"
]
}
}
],
"items": {
......
......@@ -192,10 +192,11 @@ paths:
invalidArguments:
type: "array"
example:
- name: "argument"
value: 100
validationErrors: