2727use Riverline \MultiPartParser \Converters \PSR7 ;
2828use Riverline \MultiPartParser \StreamedPart ;
2929
30+ use function array_diff_assoc ;
31+ use function array_map ;
3032use function array_replace ;
33+ use function array_shift ;
34+ use function explode ;
3135use function in_array ;
3236use function is_array ;
3337use function json_decode ;
3640use function preg_match ;
3741use function str_replace ;
3842use function strpos ;
43+ use function strtolower ;
44+ use function substr ;
3945
4046use const JSON_ERROR_NONE ;
4147
@@ -71,7 +77,7 @@ public function validate(OperationAddress $addr, MessageInterface $message): voi
7177
7278 // 0. Multipart body message MUST be described with a set of object properties
7379 if ($ schema ->type !== CebeType::OBJECT ) {
74- throw TypeMismatch::becauseTypeDoesNotMatch ('object ' , $ schema ->type );
80+ throw TypeMismatch::becauseTypeDoesNotMatch ([ 'object ' ] , $ schema ->type );
7581 }
7682
7783 if ($ message ->getBody ()->getSize ()) {
@@ -109,27 +115,20 @@ private function validatePlainBodyMultipart(
109115
110116 foreach ($ parts as $ part ) {
111117 // 2.1 parts encoding
112- $ partContentType = $ part ->getHeader (self ::HEADER_CONTENT_TYPE );
113- $ encodingContentType = $ this ->detectEncondingContentType ($ encoding , $ part , $ schema ->properties [$ partName ]);
114- if (strpos ($ encodingContentType , '* ' ) === false ) {
115- // strict comparison (ie "image/jpeg")
116- if ($ encodingContentType !== $ partContentType ) {
117- throw InvalidBody::becauseBodyDoesNotMatchSchemaMultipart (
118- $ partName ,
119- $ partContentType ,
120- $ addr
121- );
122- }
123- } else {
124- // loose comparison (ie "image/*")
125- $ encodingContentType = str_replace ('* ' , '.* ' , $ encodingContentType );
126- if (! preg_match ('# ' . $ encodingContentType . '# ' , $ partContentType )) {
127- throw InvalidBody::becauseBodyDoesNotMatchSchemaMultipart (
128- $ partName ,
129- $ partContentType ,
130- $ addr
131- );
132- }
118+ $ partContentType = $ part ->getHeader (self ::HEADER_CONTENT_TYPE );
119+ $ validContentTypes = $ this ->detectEncodingContentTypes ($ encoding , $ part , $ schema ->properties [$ partName ]);
120+ $ match = false ;
121+
122+ foreach ($ validContentTypes as $ contentType ) {
123+ $ match = $ match || $ this ->contentTypeMatches ($ contentType , $ partContentType );
124+ }
125+
126+ if (! $ match ) {
127+ throw InvalidBody::becauseBodyDoesNotMatchSchemaMultipart (
128+ $ partName ,
129+ $ partContentType ,
130+ $ addr
131+ );
133132 }
134133
135134 // 2.2. parts headers
@@ -187,7 +186,10 @@ private function parseMultipartData(OperationAddress $addr, StreamedPart $docume
187186 return $ multipartData ;
188187 }
189188
190- private function detectEncondingContentType (Encoding $ encoding , StreamedPart $ part , Schema $ partSchema ): string
189+ /**
190+ * @return string[]
191+ */
192+ private function detectEncodingContentTypes (Encoding $ encoding , StreamedPart $ part , Schema $ partSchema ): array
191193 {
192194 $ contentType = $ encoding ->contentType ;
193195
@@ -211,7 +213,69 @@ private function detectEncondingContentType(Encoding $encoding, StreamedPart $pa
211213 }
212214 }
213215
214- return $ contentType ;
216+ return array_map ('trim ' , explode (', ' , $ contentType ));
217+ }
218+
219+ private function contentTypeMatches (string $ expected , string $ match ): bool
220+ {
221+ $ expectedNormalized = $ this ->normalizedContentTypeParts ($ expected );
222+ $ matchNormalized = $ this ->normalizedContentTypeParts ($ match );
223+ $ expectedType = array_shift ($ expectedNormalized );
224+ $ matchType = array_shift ($ matchNormalized );
225+
226+ if (strpos ($ expectedType , '* ' ) === false ) {
227+ // strict comparison (ie "image/jpeg")
228+ if ($ expectedType !== $ matchType ) {
229+ return false ;
230+ }
231+ } else {
232+ // loose comparison (ie "image/*")
233+ $ expectedType = str_replace ('* ' , '.* ' , $ expectedType );
234+ if (! preg_match ('# ' . $ expectedType . '# ' , $ matchType )) {
235+ return false ;
236+ }
237+ }
238+
239+ // Any expected parameter values must also match
240+ return ! array_diff_assoc ($ expectedNormalized , $ matchNormalized );
241+ }
242+
243+ /**
244+ * Per RFC-7231 Section 3.1.1.1:
245+ * "The type, subtype, and parameter name tokens are case-insensitive. Parameter values might or might not be case-sensitive..."
246+ *
247+ * And section 3.1.1.2: "A charset is identified by a case-insensitive token."
248+ *
249+ * The following are equivalent:
250+ *
251+ * text/html;charset=utf-8
252+ * text/html;charset=UTF-8
253+ * Text/HTML;Charset="utf-8"
254+ * text/html; charset="utf-8"
255+ *
256+ * @return array<int|string, string>
257+ */
258+ private function normalizedContentTypeParts (string $ contentType ): array
259+ {
260+ $ parts = array_map ('trim ' , explode ('; ' , $ contentType ));
261+ $ result = [strtolower (array_shift ($ parts ))];
262+
263+ foreach ($ parts as $ part ) {
264+ [$ parameter , $ value ] = explode ('= ' , $ part , 2 );
265+ $ parameter = strtolower ($ parameter );
266+
267+ if ($ value [0 ] === '" ' ) { // quoted-string
268+ $ value = str_replace ('\\' , '' , substr ($ value , 1 , -1 ));
269+ }
270+
271+ if ($ parameter === 'charset ' ) {
272+ $ value = strtolower ($ value );
273+ }
274+
275+ $ result [$ parameter ] = $ value ;
276+ }
277+
278+ return $ result ;
215279 }
216280
217281 /**
0 commit comments