Skip to content

Commit

Permalink
Merge branch '44-improve-body-multipart-validation'
Browse files Browse the repository at this point in the history
  • Loading branch information
lezhnev74 committed Jul 18, 2019
2 parents a7aed23 + 67bb4f4 commit 831daf4
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 47 deletions.
4 changes: 2 additions & 2 deletions src/PSR7/Exception/Validation/AddressValidationFailed.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ abstract class AddressValidationFailed extends ValidationFailed
/**
* @return static
*/
public static function fromAddrAndPrev(OperationAddress $address, ?Throwable $prev) : self
public static function fromAddrAndPrev(OperationAddress $address, Throwable $prev) : self
{
$ex = new static(sprintf('Validation failed for %s', $address), $prev ? $prev->getCode() : 0, $prev);
$ex = new static(sprintf('Validation failed for %s', $address), $prev->getCode(), $prev);
$ex->address = $address;

return $ex;
Expand Down
2 changes: 1 addition & 1 deletion src/PSR7/Exception/Validation/InvalidBody.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static function becauseBodyDoesNotMatchSchemaMultipart(
OperationAddress $addr,
?SchemaMismatch $prev = null
) : self {
$exception = static::fromAddrAndPrev($addr, $prev);
$exception = $prev ? static::fromAddrAndPrev($addr, $prev) : static::fromAddr($addr);
$exception->message = sprintf(
'Multipart body does not match schema for part "%s" with content-type "%s" for %s',
$partName,
Expand Down
17 changes: 4 additions & 13 deletions src/PSR7/Validators/BodyValidator/BodyValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace OpenAPIValidation\PSR7\Validators\BodyValidator;

use OpenAPIValidation\PSR7\Exception\NoPath;
use OpenAPIValidation\PSR7\Exception\Validation\InvalidBody;
use OpenAPIValidation\PSR7\Exception\Validation\InvalidHeaders;
use OpenAPIValidation\PSR7\MessageValidator;
use OpenAPIValidation\PSR7\OperationAddress;
Expand All @@ -23,9 +21,6 @@ final class BodyValidator implements MessageValidator
{
private const HEADER_CONTENT_TYPE = 'Content-Type';
use ValidationStrategy;
use MultipartValidation;
use UnipartValidation;
use FormUrlencodedValidation;

/** @var SpecFinder */
private $finder;
Expand All @@ -35,11 +30,7 @@ public function __construct(SpecFinder $finder)
$this->finder = $finder;
}

/**
* @throws InvalidBody
* @throws InvalidHeaders
* @throws NoPath
*/
/** {@inheritdoc} */
public function validate(OperationAddress $addr, MessageInterface $message) : void
{
$mediaTypeSpecs = $this->finder->findBodySpec($addr);
Expand Down Expand Up @@ -70,11 +61,11 @@ public function validate(OperationAddress $addr, MessageInterface $message) : vo

// Validate message body
if (preg_match('#^multipart/.*#', $contentType)) {
$this->validateMultipart($addr, $message, $mediaTypeSpecs, $contentType);
(new MultipartValidator($mediaTypeSpecs[$contentType], $contentType))->validate($addr, $message);
} elseif (preg_match('#^application/x-www-form-urlencoded$#', $contentType)) {
$this->validateFormUrlencoded($addr, $message, $mediaTypeSpecs, $contentType);
(new FormUrlencodedValidator($mediaTypeSpecs[$contentType], $contentType))->validate($addr, $message);
} else {
$this->validateUnipart($addr, $message, $mediaTypeSpecs, $contentType);
(new UnipartValidator($mediaTypeSpecs[$contentType], $contentType))->validate($addr, $message);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
use cebe\openapi\spec\MediaType;
use cebe\openapi\spec\Schema;
use cebe\openapi\spec\Type as CebeType;
use OpenAPIValidation\PSR7\Exception\NoPath;
use OpenAPIValidation\PSR7\Exception\Validation\InvalidBody;
use OpenAPIValidation\PSR7\Exception\ValidationFailed;
use OpenAPIValidation\PSR7\MessageValidator;
use OpenAPIValidation\PSR7\OperationAddress;
use OpenAPIValidation\PSR7\Validators\ValidationStrategy;
use OpenAPIValidation\Schema\Exception\SchemaMismatch;
use OpenAPIValidation\Schema\Exception\TypeMismatch;
use OpenAPIValidation\Schema\SchemaValidator;
Expand All @@ -18,17 +22,29 @@
/**
* Should validate "application/x-www-form-urlencoded" body types
*/
trait FormUrlencodedValidation
class FormUrlencodedValidator implements MessageValidator
{
use ValidationStrategy;

/** @var MediaType */
protected $mediaTypeSpec;
/** @var string */
protected $contentType;

public function __construct(MediaType $mediaTypeSpec, string $contentType)
{
$this->mediaTypeSpec = $mediaTypeSpec;
$this->contentType = $contentType;
}

/**
* @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#requestBodyObject
*
* @param MediaType[] $mediaTypeSpecs
* @throws NoPath
* @throws ValidationFailed
*/
private function validateFormUrlencoded(OperationAddress $addr, MessageInterface $message, array $mediaTypeSpecs, string $contentType) : void
public function validate(OperationAddress $addr, MessageInterface $message) : void
{
/** @var Schema $schema */
$schema = $mediaTypeSpecs[$contentType]->schema;
$schema = $this->mediaTypeSpec->schema;

// 0. Multipart body message MUST be described with a set of object properties
if ($schema->type !== CebeType::OBJECT) {
Expand All @@ -42,15 +58,15 @@ private function validateFormUrlencoded(OperationAddress $addr, MessageInterface
try {
$validator->validate($body, $schema);
} catch (SchemaMismatch $e) {
throw InvalidBody::becauseBodyDoesNotMatchSchema($contentType, $addr, $e);
throw InvalidBody::becauseBodyDoesNotMatchSchema($this->contentType, $addr, $e);
}

// 3. Validate specified part encodings and headers
// @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#encoding-object
// The encoding object SHALL only apply to requestBody objects when the media type is multipart or application/x-www-form-urlencoded.
// An encoding attribute is introduced to give you control over the serialization of parts of multipart request bodies.
// This attribute is only applicable to "multipart" and "application/x-www-form-urlencoded" request bodies.
$encodings = $mediaTypeSpecs[$contentType]->encoding;
$encodings = $this->mediaTypeSpec->encoding;

// todo URL Serialization:
// @see https://github.com/lezhnev74/openapi-psr7-validator/issues/47
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@
use cebe\openapi\spec\MediaType;
use cebe\openapi\spec\Schema;
use cebe\openapi\spec\Type as CebeType;
use OpenAPIValidation\PSR7\Exception\NoPath;
use OpenAPIValidation\PSR7\Exception\Validation\InvalidBody;
use OpenAPIValidation\PSR7\Exception\Validation\InvalidHeaders;
use OpenAPIValidation\PSR7\Exception\ValidationFailed;
use OpenAPIValidation\PSR7\MessageValidator;
use OpenAPIValidation\PSR7\OperationAddress;
use OpenAPIValidation\PSR7\Validators\ValidationStrategy;
use OpenAPIValidation\Schema\Exception\SchemaMismatch;
use OpenAPIValidation\Schema\Exception\TypeMismatch;
use OpenAPIValidation\Schema\SchemaValidator;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Riverline\MultiPartParser\Converters\PSR7;
use Riverline\MultiPartParser\StreamedPart;
use RuntimeException;
Expand All @@ -32,43 +38,70 @@
/**
* Should validate multipart/* body types
*/
trait MultipartValidation
class MultipartValidator implements MessageValidator
{
use ValidationStrategy;

private const HEADER_CONTENT_TYPE = 'Content-Type';

/** @var MediaType */
protected $mediaTypeSpec;
/** @var string */
protected $contentType;

public function __construct(MediaType $mediaTypeSpec, string $contentType)
{
$this->mediaTypeSpec = $mediaTypeSpec;
$this->contentType = $contentType;
}

/**
* @see https://swagger.io/docs/specification/describing-request-body/multipart-requests/
* @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#requestBodyObject
*
* @param MediaType[] $mediaTypeSpecs
* @throws NoPath
* @throws ValidationFailed
*/
private function validateMultipart(OperationAddress $addr, MessageInterface $message, array $mediaTypeSpecs, string $contentType) : void
public function validate(OperationAddress $addr, MessageInterface $message) : void
{
/** @var Schema $schema */
$schema = $mediaTypeSpecs[$contentType]->schema;
$schema = $this->mediaTypeSpec->schema;

// 0. Multipart body message MUST be described with a set of object properties
if ($schema->type !== CebeType::OBJECT) {
throw TypeMismatch::becauseTypeDoesNotMatch('object', $schema->type);
}

if (($message instanceof ResponseInterface) || $message->getBody()->getSize()) {
$this->validatePlainBodyMultipart($addr, $message, $schema);
} elseif ($message instanceof ServerRequestInterface) {
$this->validateServerRequestMultipart($addr, $message, $schema);
}
}

/**
* @param MediaType[] $mediaTypeSpecs
*/
private function validatePlainBodyMultipart(
OperationAddress $addr,
MessageInterface $message,
Schema $schema
) : void {
// 1. Parse message body
$document = PSR7::convert($message);

// 2. Validate bodies of each part
$body = $this->parseMultipartData($addr, $document);

$validator = new SchemaValidator($this->detectValidationStrategy($message));
try {
$validator->validate($body, $schema);
} catch (SchemaMismatch $e) {
throw InvalidBody::becauseBodyDoesNotMatchSchema($contentType, $addr, $e);
throw InvalidBody::becauseBodyDoesNotMatchSchema($this->contentType, $addr, $e);
}

// 3. Validate specified part encodings and headers
// 2. Validate specified part encodings and headers
// @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#encoding-object
// The encoding object SHALL only apply to requestBody objects when the media type is multipart or application/x-www-form-urlencoded.
// An encoding attribute is introduced to give you control over the serialization of parts of multipart request bodies.
// This attribute is only applicable to "multipart" and "application/x-www-form-urlencoded" request bodies.
$encodings = $mediaTypeSpecs[$contentType]->encoding;
$encodings = $this->mediaTypeSpec->encoding;

foreach ($encodings as $partName => $encoding) {
$parts = $document->getPartsByName($partName); // multiple parts share a name?
Expand All @@ -80,7 +113,7 @@ private function validateMultipart(OperationAddress $addr, MessageInterface $mes
}

foreach ($parts as $part) {
// 3.1 parts encoding
// 2.1 parts encoding
$partContentType = $part->getHeader(self::HEADER_CONTENT_TYPE);
$encodingContentType = $this->detectEncondingContentType($encoding, $part, $schema->properties[$partName]);
if (strpos($encodingContentType, '*') === false) {
Expand All @@ -104,7 +137,7 @@ private function validateMultipart(OperationAddress $addr, MessageInterface $mes
}
}

// 3.2. parts headers
// 2.2. parts headers
foreach ($encoding->headers as $headerName => $headerSpec) {
/** @var Header $headerSpec */
$headerSchema = $headerSpec->schema;
Expand Down Expand Up @@ -184,4 +217,51 @@ private function detectEncondingContentType(Encoding $encoding, StreamedPart $pa

return $contentType;
}

/**
* ServerRequest does not have a plain HTTP body which we can parse. Instead, it has a parsed values in
* getParsedBody() (POST data) and getUploadedFiles (FILES data)
*
* @param MediaType[] $mediaTypeSpecs
*/
private function validateServerRequestMultipart(
OperationAddress $addr,
ServerRequestInterface $message,
Schema $schema
) : void {
// add parsed simple values
$body = $message->getParsedBody();

// add files as binary strings
foreach ($message->getUploadedFiles() as $name => $file) {
$body[$name] = '~~~binary~~~';
}

$validator = new SchemaValidator($this->detectValidationStrategy($message));
try {
$validator->validate($body, $schema);
} catch (SchemaMismatch $e) {
throw InvalidBody::becauseBodyDoesNotMatchSchema($this->contentType, $addr, $e);
}

// 2. Validate specified part encodings and headers
// @see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#encoding-object
// The encoding object SHALL only apply to requestBody objects when the media type is multipart or application/x-www-form-urlencoded.
// An encoding attribute is introduced to give you control over the serialization of parts of multipart request bodies.
// This attribute is only applicable to "multipart" and "application/x-www-form-urlencoded" request bodies.
$encodings = $this->mediaTypeSpec->encoding;

foreach ($encodings as $partName => $encoding) {
if (! isset($body[$partName])) {
throw new RuntimeException(sprintf('Specified body part %s is not found', $partName));
}
$part = $body[$partName];

// 2.1 parts encoding
// todo values are parsed already by php core...

// 2.2. parts headers
// todo headers are parsed already by webserver...
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
namespace OpenAPIValidation\PSR7\Validators\BodyValidator;

use cebe\openapi\spec\MediaType;
use OpenAPIValidation\PSR7\Exception\NoPath;
use OpenAPIValidation\PSR7\Exception\Validation\InvalidBody;
use OpenAPIValidation\PSR7\Exception\ValidationFailed;
use OpenAPIValidation\PSR7\MessageValidator;
use OpenAPIValidation\PSR7\OperationAddress;
use OpenAPIValidation\PSR7\Validators\ValidationStrategy;
use OpenAPIValidation\Schema\Exception\SchemaMismatch;
use OpenAPIValidation\Schema\SchemaValidator;
use Psr\Http\Message\MessageInterface;
Expand All @@ -16,16 +20,28 @@
use function json_last_error_msg;
use function preg_match;

trait UnipartValidation
class UnipartValidator implements MessageValidator
{
use ValidationStrategy;

/** @var MediaType */
protected $mediaTypeSpec;
/** @var string */
protected $contentType;

public function __construct(MediaType $mediaTypeSpec, string $contentType)
{
$this->mediaTypeSpec = $mediaTypeSpec;
$this->contentType = $contentType;
}

/**
* @param MediaType[] $mediaTypeSpecs
*
* @throws InvalidBody
* @throws NoPath
* @throws ValidationFailed
*/
private function validateUnipart(OperationAddress $addr, MessageInterface $message, array $mediaTypeSpecs, string $contentType) : void
public function validate(OperationAddress $addr, MessageInterface $message) : void
{
if (preg_match('#^application/.*json$#', $contentType)) {
if (preg_match('#^application/.*json$#', $this->contentType)) {
$body = json_decode((string) $message->getBody(), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw InvalidBody::becauseBodyIsNotValidJson(json_last_error_msg(), $addr);
Expand All @@ -35,11 +51,11 @@ private function validateUnipart(OperationAddress $addr, MessageInterface $messa
}

$validator = new SchemaValidator($this->detectValidationStrategy($message));
$schema = $mediaTypeSpecs[$contentType]->schema;
$schema = $this->mediaTypeSpec->schema;
try {
$validator->validate($body, $schema);
} catch (SchemaMismatch $e) {
throw InvalidBody::becauseBodyDoesNotMatchSchema($contentType, $addr, $e);
throw InvalidBody::becauseBodyDoesNotMatchSchema($this->contentType, $addr, $e);
}
}
}
4 changes: 2 additions & 2 deletions src/Schema/TypeFormats/StringUUID.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class StringUUID
{
public function __invoke(string $value) : bool
{
$patternUUIDV4 = '/^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$/';
$pattern = '/^[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}$/i';

return (bool) preg_match($patternUUIDV4, $value);
return (bool) preg_match($pattern, $value);
}
}
Loading

0 comments on commit 831daf4

Please sign in to comment.