Skip to content

Commit ca95cf6

Browse files
reznikartemybelenko
authored andcommitted
[php-symfony] Fix problem with clients, that put charset in content type header. (OpenAPITools#6078)
* Fix problem with clients, that put charset in content type header. With this fix header "Content-Type: application/json; charset=utf-8" working same as "Content-Type: application/json" for parse input data * Fix code style, add $consumes length check. * Add isContentTypeAllowed static method and tests * Fix old tests Right now serializer doesn't support anything beside json and xml. Call tests with application/json instead of form data. Co-authored-by: Yuriy Belenko <[email protected]>
1 parent d0cfc5b commit ca95cf6

File tree

15 files changed

+344
-24
lines changed

15 files changed

+344
-24
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSymfonyServerCodegen.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class PhpSymfonyServerCodegen extends AbstractPhpCodegen implements Codeg
5555
protected String controllerDirName = "Controller";
5656
protected String serviceDirName = "Service";
5757
protected String controllerPackage;
58+
protected String controllerTestsPackage;
5859
protected String servicePackage;
5960
protected Boolean phpLegacySupport = Boolean.TRUE;
6061

@@ -301,6 +302,7 @@ public void processOpts() {
301302
additionalProperties.put("servicePackage", servicePackage);
302303
additionalProperties.put("apiTestsPackage", apiTestsPackage);
303304
additionalProperties.put("modelTestsPackage", modelTestsPackage);
305+
additionalProperties.put("controllerTestsPackage", controllerTestsPackage);
304306

305307
// make Symonfy-specific properties available
306308
additionalProperties.put("bundleName", bundleName);
@@ -311,11 +313,13 @@ public void processOpts() {
311313
// make api and model src path available in mustache template
312314
additionalProperties.put("apiSrcPath", "." + "/" + toSrcPath(apiPackage, srcBasePath));
313315
additionalProperties.put("modelSrcPath", "." + "/" + toSrcPath(modelPackage, srcBasePath));
316+
additionalProperties.put("controllerSrcPath", "." + "/" + toSrcPath(controllerPackage, srcBasePath));
314317
additionalProperties.put("testsSrcPath", "." + "/" + toSrcPath(testsPackage, srcBasePath));
315318
additionalProperties.put("apiTestsSrcPath", "." + "/" + toSrcPath(apiTestsPackage, srcBasePath));
316319
additionalProperties.put("modelTestsSrcPath", "." + "/" + toSrcPath(modelTestsPackage, srcBasePath));
317320
additionalProperties.put("apiTestPath", "." + "/" + testsDirName + "/" + apiDirName);
318321
additionalProperties.put("modelTestPath", "." + "/" + testsDirName + "/" + modelDirName);
322+
additionalProperties.put("controllerTestPath", "." + "/" + testsDirName + "/" + controllerDirName);
319323

320324
// make api and model doc path available in mustache template
321325
additionalProperties.put("apiDocPath", apiDocPath);
@@ -346,6 +350,7 @@ public void processOpts() {
346350
supportingFiles.add(new SupportingFile("testing/phpunit.xml.mustache", "", "phpunit.xml.dist"));
347351
supportingFiles.add(new SupportingFile("testing/pom.xml", "", "pom.xml"));
348352
supportingFiles.add(new SupportingFile("testing/AppKernel.php", toSrcPath(testsPackage, srcBasePath), "AppKernel.php"));
353+
supportingFiles.add(new SupportingFile("testing/ControllerTest.mustache", toSrcPath(controllerTestsPackage, srcBasePath), "ControllerTest.php"));
349354
supportingFiles.add(new SupportingFile("testing/test_config.yml", toSrcPath(testsPackage, srcBasePath), "test_config.yml"));
350355

351356
supportingFiles.add(new SupportingFile("routing.mustache", configDir, "routing.yml"));
@@ -540,6 +545,7 @@ public void setInvokerPackage(String invokerPackage) {
540545
apiTestsPackage = testsPackage + "\\" + apiDirName;
541546
modelTestsPackage = testsPackage + "\\" + modelDirName;
542547
controllerPackage = invokerPackage + "\\" + controllerDirName;
548+
controllerTestsPackage = testsPackage + "\\" + controllerDirName;
543549
servicePackage = invokerPackage + "\\" + serviceDirName;
544550
}
545551

modules/openapi-generator/src/main/resources/php-symfony/Controller.mustache

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
namespace {{controllerPackage}};
2121

2222
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
23+
use Symfony\Component\HttpFoundation\Request;
2324
use Symfony\Component\HttpFoundation\Response;
2425
use Symfony\Component\HttpKernel\Exception\HttpException;
2526
use {{servicePackage}}\SerializerInterface;
@@ -176,4 +177,40 @@ class Controller extends AbstractController
176177
// If we reach this point, we don't have a common ground between server and client
177178
return null;
178179
}
180+
181+
/**
182+
* Checks whether Content-Type request header presented in supported formats.
183+
*
184+
* @param Request $request Request instance.
185+
* @param array $consumes Array of supported content types.
186+
*
187+
* @return bool Returns true if Content-Type supported otherwise false.
188+
*/
189+
public static function isContentTypeAllowed(Request $request, array $consumes = [])
190+
{
191+
if (!empty($consumes) && $consumes[0] !== '*/*') {
192+
$currentFormat = $request->getContentType();
193+
foreach ($consumes as $mimeType) {
194+
// canonize mime type
195+
if (is_string($mimeType) && false !== $pos = strpos($mimeType, ';')) {
196+
$mimeType = trim(substr($mimeType, 0, $pos));
197+
}
198+
199+
if (!$format = $request->getFormat($mimeType)) {
200+
// add custom format to request
201+
$format = $mimeType;
202+
$request->setFormat($format, $format);
203+
$currentFormat = $request->getContentType();
204+
}
205+
206+
if ($format === $currentFormat) {
207+
return true;
208+
}
209+
}
210+
211+
return false;
212+
}
213+
214+
return true;
215+
}
179216
}

modules/openapi-generator/src/main/resources/php-symfony/api_controller.mustache

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ class {{controllerName}} extends Controller
6060
{{#bodyParams}}
6161
// Make sure that the client is providing something that we can consume
6262
$consumes = [{{#consumes}}'{{{mediaType}}}'{{#hasMore}}, {{/hasMore}}{{/consumes}}];
63-
$inputFormat = $request->headers->has('Content-Type')?$request->headers->get('Content-Type'):$consumes[0];
64-
if (!in_array($inputFormat, $consumes)) {
63+
if (!static::isContentTypeAllowed($request, $consumes)) {
6564
// We can't consume the content that the client is sending us
6665
return new Response('', 415);
6766
}
@@ -146,6 +145,7 @@ class {{controllerName}} extends Controller
146145
{{#allParams}}
147146
{{^isFile}}
148147
{{#isBodyParam}}
148+
$inputFormat = $request->getMimeType($request->getContentType());
149149
${{paramName}} = $this->deserialize(${{paramName}}, '{{#isContainer}}{{#items}}array<{{dataType}}>{{/items}}{{/isContainer}}{{^isContainer}}{{dataType}}{{/isContainer}}', $inputFormat);
150150
{{/isBodyParam}}
151151
{{^isBodyParam}}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
/**
3+
* ControllerTest
4+
* PHP version 5
5+
*
6+
* @category Class
7+
* @package {{controllerTestsPackage}}
8+
* @author openapi-generator contributors
9+
* @link https://github.com/openapitools/openapi-generator
10+
*/
11+
12+
{{>partial_header}}
13+
/**
14+
* NOTE: This class is auto generated by the openapi generator program.
15+
* https://github.com/openapitools/openapi-generator
16+
* Please update the test case below to test the endpoint.
17+
*/
18+
19+
namespace {{controllerTestsPackage}};
20+
21+
use {{controllerPackage}}\Controller;
22+
use PHPUnit\Framework\TestCase;
23+
use Symfony\Component\HttpFoundation\Request;
24+
25+
/**
26+
* ControllerTest Class Doc Comment
27+
*
28+
* @category Class
29+
* @package {{controllerTestsPackage}}
30+
* @author openapi-generator contributors
31+
* @link https://github.com/openapitools/openapi-generator
32+
* @coversDefaultClass \{{controllerPackage}}\Controller
33+
*/
34+
class ControllerTest extends TestCase
35+
{
36+
37+
/**
38+
* Tests isContentTypeAllowed static method.
39+
*
40+
* @param string $contentType
41+
* @param array $consumes
42+
* @param bool $expectedReturn
43+
*
44+
* @covers ::isContentTypeAllowed
45+
* @dataProvider provideArgumentsForIsContentTypeAllowed
46+
*/
47+
public function testIsContentTypeAllowed($contentType, array $consumes, $expectedReturn)
48+
{
49+
$request = new Request();
50+
$request->headers->set('CONTENT_TYPE', $contentType, true);// last one argument overrides header
51+
$this->assertSame(
52+
$expectedReturn,
53+
Controller::isContentTypeAllowed($request, $consumes),
54+
sprintf(
55+
'Failed assertion that "Content-Type: %s" %s by [%s] consumes array.',
56+
$contentType,
57+
($expectedReturn) ? 'is allowed' : 'is forbidden',
58+
implode(', ', $consumes)
59+
)
60+
);
61+
}
62+
63+
public function provideArgumentsForIsContentTypeAllowed()
64+
{
65+
return [
66+
'usual JSON content type' => [
67+
'application/json',
68+
['application/json'],
69+
true,
70+
],
71+
'extended content type from PR #6078' => [
72+
'application/json; charset=utf-8',
73+
['application/json'],
74+
true,
75+
],
76+
'more than one content types' => [
77+
'application/json',
78+
['application/xml', 'application/json; charset=utf-8'],
79+
true,
80+
],
81+
'empty consumes array' => [
82+
'application/json',
83+
[],
84+
true,
85+
],
86+
'empty consumes and content type' => [
87+
null,
88+
[],
89+
true,
90+
],
91+
'consumes everything' => [
92+
'application/json',
93+
['*/*'],
94+
true,
95+
],
96+
'fancy custom content type' => [
97+
'foobar/foobaz',
98+
['application/xml', 'foobar/foobaz; charset=utf-8'],
99+
true,
100+
],
101+
'empty content type' => [
102+
null,
103+
['application/xml', 'application/json; charset=utf-8'],
104+
false,
105+
],
106+
'content type out of consumes' => [
107+
'text/html',
108+
['application/xml', 'application/json; charset=utf-8'],
109+
false,
110+
],
111+
];
112+
}
113+
}

modules/openapi-generator/src/main/resources/php-symfony/testing/api_test.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
9999
$path = str_replace($pattern, $data, $path);
100100
{{/pathParams}}
101101

102-
$crawler = $client->request('{{httpMethod}}', $path);
102+
$crawler = $client->request('{{httpMethod}}', $path{{#hasBodyParam}}, [], [], ['CONTENT_TYPE' => 'application/json']{{/hasBodyParam}});
103103
}
104104
{{/operation}}
105105

modules/openapi-generator/src/main/resources/php-symfony/testing/phpunit.xml.mustache

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
<testsuite>
1010
<directory>{{apiTestPath}}</directory>
1111
<directory>{{modelTestPath}}</directory>
12+
<directory>{{controllerTestPath}}</directory>
1213
</testsuite>
1314
</testsuites>
1415
<filter>
1516
<whitelist processUncoveredFilesFromWhitelist="true">
1617
<directory suffix=".php">{{apiSrcPath}}</directory>
1718
<directory suffix=".php">{{modelSrcPath}}</directory>
19+
<directory suffix=".php">{{controllerSrcPath}}</directory>
1820
</whitelist>
1921
</filter>
2022
<php>

samples/server/petstore/php-symfony/SymfonyBundle-php/Controller/Controller.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
namespace OpenAPI\Server\Controller;
3131

3232
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
33+
use Symfony\Component\HttpFoundation\Request;
3334
use Symfony\Component\HttpFoundation\Response;
3435
use Symfony\Component\HttpKernel\Exception\HttpException;
3536
use OpenAPI\Server\Service\SerializerInterface;
@@ -186,4 +187,40 @@ protected function getOutputFormat($accept, array $produced)
186187
// If we reach this point, we don't have a common ground between server and client
187188
return null;
188189
}
190+
191+
/**
192+
* Checks whether Content-Type request header presented in supported formats.
193+
*
194+
* @param Request $request Request instance.
195+
* @param array $consumes Array of supported content types.
196+
*
197+
* @return bool Returns true if Content-Type supported otherwise false.
198+
*/
199+
public static function isContentTypeAllowed(Request $request, array $consumes = [])
200+
{
201+
if (!empty($consumes) && $consumes[0] !== '*/*') {
202+
$currentFormat = $request->getContentType();
203+
foreach ($consumes as $mimeType) {
204+
// canonize mime type
205+
if (is_string($mimeType) && false !== $pos = strpos($mimeType, ';')) {
206+
$mimeType = trim(substr($mimeType, 0, $pos));
207+
}
208+
209+
if (!$format = $request->getFormat($mimeType)) {
210+
// add custom format to request
211+
$format = $mimeType;
212+
$request->setFormat($format, $format);
213+
$currentFormat = $request->getContentType();
214+
}
215+
216+
if ($format === $currentFormat) {
217+
return true;
218+
}
219+
}
220+
221+
return false;
222+
}
223+
224+
return true;
225+
}
189226
}

samples/server/petstore/php-symfony/SymfonyBundle-php/Controller/PetController.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ public function addPetAction(Request $request)
6262
{
6363
// Make sure that the client is providing something that we can consume
6464
$consumes = ['application/json', 'application/xml'];
65-
$inputFormat = $request->headers->has('Content-Type')?$request->headers->get('Content-Type'):$consumes[0];
66-
if (!in_array($inputFormat, $consumes)) {
65+
if (!static::isContentTypeAllowed($request, $consumes)) {
6766
// We can't consume the content that the client is sending us
6867
return new Response('', 415);
6968
}
@@ -80,6 +79,7 @@ public function addPetAction(Request $request)
8079

8180
// Deserialize the input values that needs it
8281
try {
82+
$inputFormat = $request->getMimeType($request->getContentType());
8383
$body = $this->deserialize($body, 'OpenAPI\Server\Model\Pet', $inputFormat);
8484
} catch (SerializerRuntimeException $exception) {
8585
return $this->createBadRequestResponse($exception->getMessage());
@@ -491,8 +491,7 @@ public function updatePetAction(Request $request)
491491
{
492492
// Make sure that the client is providing something that we can consume
493493
$consumes = ['application/json', 'application/xml'];
494-
$inputFormat = $request->headers->has('Content-Type')?$request->headers->get('Content-Type'):$consumes[0];
495-
if (!in_array($inputFormat, $consumes)) {
494+
if (!static::isContentTypeAllowed($request, $consumes)) {
496495
// We can't consume the content that the client is sending us
497496
return new Response('', 415);
498497
}
@@ -509,6 +508,7 @@ public function updatePetAction(Request $request)
509508

510509
// Deserialize the input values that needs it
511510
try {
511+
$inputFormat = $request->getMimeType($request->getContentType());
512512
$body = $this->deserialize($body, 'OpenAPI\Server\Model\Pet', $inputFormat);
513513
} catch (SerializerRuntimeException $exception) {
514514
return $this->createBadRequestResponse($exception->getMessage());

samples/server/petstore/php-symfony/SymfonyBundle-php/Controller/StoreController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,7 @@ public function placeOrderAction(Request $request)
284284
{
285285
// Make sure that the client is providing something that we can consume
286286
$consumes = [];
287-
$inputFormat = $request->headers->has('Content-Type')?$request->headers->get('Content-Type'):$consumes[0];
288-
if (!in_array($inputFormat, $consumes)) {
287+
if (!static::isContentTypeAllowed($request, $consumes)) {
289288
// We can't consume the content that the client is sending us
290289
return new Response('', 415);
291290
}
@@ -308,6 +307,7 @@ public function placeOrderAction(Request $request)
308307

309308
// Deserialize the input values that needs it
310309
try {
310+
$inputFormat = $request->getMimeType($request->getContentType());
311311
$body = $this->deserialize($body, 'OpenAPI\Server\Model\Order', $inputFormat);
312312
} catch (SerializerRuntimeException $exception) {
313313
return $this->createBadRequestResponse($exception->getMessage());

0 commit comments

Comments
 (0)