Skip to content

Commit

Permalink
(#134) Add support for comma-separated list of encoding content types
Browse files Browse the repository at this point in the history
  • Loading branch information
Jim Cottrell authored and scaytrase committed Mar 23, 2023
1 parent 367e7c0 commit 548357c
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 23 deletions.
110 changes: 87 additions & 23 deletions src/PSR7/Validators/BodyValidator/MultipartValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@
use Riverline\MultiPartParser\StreamedPart;
use RuntimeException;

use function array_diff;
use function array_map;
use function array_replace;
use function array_shift;
use function explode;
use function in_array;
use function is_array;
use function json_decode;
Expand All @@ -38,6 +42,8 @@
use function sprintf;
use function str_replace;
use function strpos;
use function strtolower;
use function substr;

use const JSON_ERROR_NONE;

Expand Down Expand Up @@ -117,27 +123,20 @@ private function validatePlainBodyMultipart(

foreach ($parts as $part) {
// 2.1 parts encoding
$partContentType = $part->getHeader(self::HEADER_CONTENT_TYPE);
$encodingContentType = $this->detectEncondingContentType($encoding, $part, $schema->properties[$partName]);
if (strpos($encodingContentType, '*') === false) {
// strict comparison (ie "image/jpeg")
if ($encodingContentType !== $partContentType) {
throw InvalidBody::becauseBodyDoesNotMatchSchemaMultipart(
$partName,
$partContentType,
$addr
);
}
} else {
// loose comparison (ie "image/*")
$encodingContentType = str_replace('*', '.*', $encodingContentType);
if (! preg_match('#' . $encodingContentType . '#', $partContentType)) {
throw InvalidBody::becauseBodyDoesNotMatchSchemaMultipart(
$partName,
$partContentType,
$addr
);
}
$partContentType = $part->getHeader(self::HEADER_CONTENT_TYPE);
$validContentTypes = $this->detectEncodingContentTypes($encoding, $part, $schema->properties[$partName]);
$match = false;

foreach ($validContentTypes as $contentType) {
$match = $match || $this->contentTypeMatches($contentType, $partContentType);
}

if (! $match) {
throw InvalidBody::becauseBodyDoesNotMatchSchemaMultipart(
$partName,
$partContentType,
$addr
);
}

// 2.2. parts headers
Expand Down Expand Up @@ -195,7 +194,10 @@ private function parseMultipartData(OperationAddress $addr, StreamedPart $docume
return $multipartData;
}

private function detectEncondingContentType(Encoding $encoding, StreamedPart $part, Schema $partSchema): string
/**
* @return string[]
*/
private function detectEncodingContentTypes(Encoding $encoding, StreamedPart $part, Schema $partSchema): array
{
$contentType = $encoding->contentType;

Expand All @@ -219,7 +221,69 @@ private function detectEncondingContentType(Encoding $encoding, StreamedPart $pa
}
}

return $contentType;
return array_map('trim', explode(',', $contentType));
}

private function contentTypeMatches(string $expected, string $match): bool
{
$expectedNormalized = $this->normalizedContentTypeParts($expected);
$matchNormalized = $this->normalizedContentTypeParts($match);
$expectedType = array_shift($expectedNormalized);
$matchType = array_shift($matchNormalized);

if (strpos($expectedType, '*') === false) {
// strict comparison (ie "image/jpeg")
if ($expectedType !== $matchType) {
return false;
}
} else {
// loose comparison (ie "image/*")
$expectedType = str_replace('*', '.*', $expectedType);
if (! preg_match('#' . $expectedType . '#', $matchType)) {
return false;
}
}

// Any expected parameter values must also match
return ! array_diff($expectedNormalized, $matchNormalized);
}

/**
* Per RFC-7231 Section 3.1.1.1:
* "The type, subtype, and parameter name tokens are case-insensitive. Parameter values might or might not be case-sensitive..."
*
* Section 3.1.1.2: "A charset is identified by a case-insensitive token."
*
* The following are equivalent:
*
* text/html;charset=utf-8
* text/html;charset=UTF-8
* Text/HTML;Charset="utf-8"
* text/html; charset="utf-8"
*
* @return array<int|string, string>
*/
private function normalizedContentTypeParts(string $contentType): array
{
$parts = array_map('trim', explode(';', $contentType));
$result = [strtolower(array_shift($parts))];

foreach ($parts as $part) {
[$parameter, $value] = explode('=', $part, 2);
$parameter = strtolower($parameter);

if ($value[0] === '"') { // quoted-string
$value = str_replace('\\', '', substr($value, 1, -1));
}

if ($parameter === 'charset') {
$value = strtolower($value);
}

$result[$parameter] = $value;
}

return $result;
}

/**
Expand Down
82 changes: 82 additions & 0 deletions tests/PSR7/Validators/BodyValidator/MultipartValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,54 @@ public function dataProviderMultipartGreen(): array
Content-Disposition: form-data; name="image"; filename="file1.txt"
Content-Type: image/whatever
[file content goes there]
------WebKitFormBoundaryWfPNVh4wuWBlyEyQ--
HTTP
,
],
// specified headers for one part (multiple, with charset)
[
<<<HTTP
POST /multipart/encoding/multiple HTTP/1.1
Content-Length: 2740
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryWfPNVh4wuWBlyEyQ
------WebKitFormBoundaryWfPNVh4wuWBlyEyQ
Content-Disposition: form-data; name="data"; filename="file1.txt"
Content-Type: APPLICATION/XML;CHARSET=UTF-8
[file content goes there]
------WebKitFormBoundaryWfPNVh4wuWBlyEyQ--
HTTP
,
],
// specified headers for one part (multiple, other valid type)
[
<<<HTTP
POST /multipart/encoding/multiple HTTP/1.1
Content-Length: 2740
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryWfPNVh4wuWBlyEyQ
------WebKitFormBoundaryWfPNVh4wuWBlyEyQ
Content-Disposition: form-data; name="data"; filename="file1.txt"
Content-Type: application/json ; charset="ISO-8859-1"
[file content goes there]
------WebKitFormBoundaryWfPNVh4wuWBlyEyQ--
HTTP
,
],
// specified headers for one part (multiple, wildcard)
[
<<<HTTP
POST /multipart/encoding/multiple HTTP/1.1
Content-Length: 2740
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryWfPNVh4wuWBlyEyQ
------WebKitFormBoundaryWfPNVh4wuWBlyEyQ
Content-Disposition: form-data; name="data"; filename="file1.txt"
Content-Type: text/plain; charset=us-ascii
[file content goes there]
------WebKitFormBoundaryWfPNVh4wuWBlyEyQ--
HTTP
Expand Down Expand Up @@ -208,6 +256,40 @@ public function dataProviderMultipartRed(): array
Content-Disposition: form-data; name="image"; filename="file1.txt"
Content-Type: invalid/type
[file content goes there]
------WebKitFormBoundaryWfPNVh4wuWBlyEyQ--
HTTP
,
InvalidBody::class,
],
// wrong encoding charset for one of the parts (multiple)
[
<<<HTTP
POST /multipart/encoding/multiple HTTP/1.1
Content-Length: 2740
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryWfPNVh4wuWBlyEyQ
------WebKitFormBoundaryWfPNVh4wuWBlyEyQ
Content-Disposition: form-data; name="data"; filename="file1.txt"
Content-Type: application/xml; charset=ISO-8859-1
[file content goes there]
------WebKitFormBoundaryWfPNVh4wuWBlyEyQ--
HTTP
,
InvalidBody::class,
],
// missing encoding charset for one of the parts (multiple)
[
<<<HTTP
POST /multipart/encoding/multiple HTTP/1.1
Content-Length: 2740
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryWfPNVh4wuWBlyEyQ
------WebKitFormBoundaryWfPNVh4wuWBlyEyQ
Content-Disposition: form-data; name="data"; filename="file1.txt"
Content-Type: application/xml
[file content goes there]
------WebKitFormBoundaryWfPNVh4wuWBlyEyQ--
HTTP
Expand Down
18 changes: 18 additions & 0 deletions tests/stubs/multipart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ paths:
responses:
204:
description: good post
/multipart/encoding/multiple:
post:
summary: ---
operationId: post-multipart-encoding-multiple
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
data:
type: string
encoding:
data:
contentType: application/xml; charset="utf-8", application/json, text/*
responses:
204:
description: good post
/multipart/encoding/wildcard:
post:
summary: Post multipart body with wildcard encoding of one part
Expand Down

0 comments on commit 548357c

Please sign in to comment.