Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Metadata/Util/ContentNegotiationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,12 @@ private function getRequestFormat(Request $request, array $formats, bool $throw
if (null !== $requestFormat) {
$mimeType = $request->getMimeType($requestFormat);

if (isset($flattenedMimeTypes[$mimeType])) {
if (null !== $mimeType && isset($flattenedMimeTypes[$mimeType])) {
return $requestFormat;
}

if ($throw) {
throw $this->getNotAcceptableHttpException($mimeType, $flattenedMimeTypes);
throw $this->getNotAcceptableHttpException($mimeType ?? $requestFormat, $flattenedMimeTypes);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,11 @@ private function registerCommonConfiguration(ContainerBuilder $container, array
$container->setParameter('api_platform.patch_formats', $patchFormats);
$container->setParameter('api_platform.error_formats', $errorFormats);
$container->setParameter('api_platform.docs_formats', $docsFormats);
// The entrypoint only has normalizers for hypermedia formats (jsonld, jsonhal, jsonapi) and a
// dedicated Swagger UI code path for html. Other documentation formats (e.g. openapi, yamlopenapi)
// have no Entrypoint normalizer and must not be advertised, otherwise content negotiation lets them
// through and the Symfony ObjectNormalizer fallback leaks the internal ResourceNameCollection FQCNs.
$container->setParameter('api_platform.entrypoint_formats', array_intersect_key($docsFormats, array_flip(['jsonld', 'jsonhal', 'jsonapi', 'html'])));
$container->setParameter('api_platform.jsonschema_formats', []);
$container->setParameter('api_platform.eager_loading.enabled', $this->isConfigEnabled($container, $config['eager_loading']));
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']);
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/Resources/config/symfony/controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
service('api_platform.metadata.resource.name_collection_factory'),
service('api_platform.state_provider.main'),
service('api_platform.state_processor.main'),
'%api_platform.docs_formats%',
'%api_platform.entrypoint_formats%',
]);

$services->set('api_platform.action.documentation', DocumentationAction::class)
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/Resources/config/symfony/events.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
service('api_platform.metadata.resource.name_collection_factory'),
service('api_platform.state_provider.documentation'),
service('api_platform.state_processor.documentation'),
'%api_platform.docs_formats%',
'%api_platform.entrypoint_formats%',
]);

$services->set('api_platform.action.documentation', DocumentationAction::class)
Expand Down
125 changes: 125 additions & 0 deletions tests/Functional/EntrypointFormatTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\Functional;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MultipleResourceBook;
use ApiPlatform\Tests\SetupClassResourcesTrait;

/**
* The entrypoint must only expose hypermedia formats that have a dedicated
* EntrypointNormalizer (jsonld, jsonhal, jsonapi). Documentation formats
* (openapi, html) have no such normalizer and must not be served, otherwise
* the Symfony ObjectNormalizer fallback dumps the public ResourceNameCollection,
* leaking internal PHP FQCNs.
*
* @see https://github.com/api-platform/core/issues/8361
*/
final class EntrypointFormatTest extends ApiTestCase
{
use SetupClassResourcesTrait;

protected static ?bool $alwaysBootKernel = false;

/**
* @return class-string[]
*/
public static function getResources(): array
{
return [MultipleResourceBook::class];
}

public function testEntrypointRejectsOpenApiAcceptHeader(): void
{
$response = self::createClient()->request('GET', '/', [
'headers' => ['accept' => 'application/vnd.openapi+json'],
]);

$this->assertResponseStatusCodeSame(406);
}

/**
* The ".jsonopenapi"/".yamlopenapi" URL suffixes are resolved by routing before
* content negotiation runs, so an unsupported route format yields a 404
* (consistent with any other resource requested with an unsupported format
* suffix), not a 406.
*/
public function testEntrypointRejectsOpenApiFormatSuffix(): void
{
$response = self::createClient()->request('GET', '/index.jsonopenapi', [
'headers' => ['accept' => 'application/vnd.openapi+json'],
]);

$this->assertResponseStatusCodeSame(404);
}

public function testEntrypointRejectsYamlOpenApiFormatSuffix(): void
{
$response = self::createClient()->request('GET', '/index.yamlopenapi', [
'headers' => ['accept' => 'application/vnd.openapi+yaml'],
]);

$this->assertResponseStatusCodeSame(404);
}

public function testEntrypointStillServesHtmlAsSwaggerUi(): void
{
$response = self::createClient()->request('GET', '/index.html', [
'headers' => ['accept' => 'text/html'],
]);

$this->assertResponseIsSuccessful();
$this->assertStringContainsString('swagger-ui', $response->getContent());
}

public function testEntrypointStillServesJsonLd(): void
{
$response = self::createClient()->request('GET', '/index.jsonld', [
'headers' => ['accept' => 'application/ld+json'],
]);

$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$data = $response->toArray();
$this->assertArrayHasKey('multipleResourceBook2', $data);
$this->assertEquals('/multi_route_books', $data['multipleResourceBook2']);
$this->assertStringNotContainsString('resourceNameCollection', $response->getContent());
}

public function testEntrypointStillServesJsonHal(): void
{
$response = self::createClient()->request('GET', '/index.jsonhal', [
'headers' => ['accept' => 'application/hal+json'],
]);

$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/hal+json; charset=utf-8');
$data = $response->toArray();
$this->assertArrayHasKey('_links', $data);
$this->assertArrayHasKey('multipleResourceBook2', $data['_links']);
}

public function testEntrypointStillServesJsonApi(): void
{
$response = self::createClient()->request('GET', '/index.jsonapi', [
'headers' => ['accept' => 'application/vnd.api+json'],
]);

$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/vnd.api+json; charset=utf-8');
$data = $response->toArray();
$this->assertArrayHasKey('links', $data);
$this->assertArrayHasKey('multipleResourceBook2', $data['links']);
}
}
22 changes: 14 additions & 8 deletions tests/Functional/OpenApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -686,24 +686,30 @@ public function testOpenApiUiIsEnabledForDocsEndpointWithDummyObject(): void
$this->assertResponseIsSuccessful();
}

public function testRetrieveTheEntrypoint(): void
/**
* @see https://github.com/api-platform/core/issues/8361
*/
public function testEntrypointRejectsOpenApiFormat(): void
{
$response = self::createClient()->request('GET', '/', [
'headers' => ['Accept' => 'application/vnd.openapi+json'],
]);
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/vnd.openapi+json; charset=utf-8');
$this->assertJson($response->getContent());
$this->assertResponseStatusCodeSame(406);
}

public function testRetrieveTheEntrypointWithUrlFormat(): void
/**
* The ".jsonopenapi" URL suffix is resolved by routing before content negotiation
* runs, so an unsupported route format yields a 404 (consistent with any other
* resource requested with an unsupported format suffix), not a 406.
*
* @see https://github.com/api-platform/core/issues/8361
*/
public function testEntrypointRejectsOpenApiFormatWithUrlFormat(): void
{
$response = self::createClient()->request('GET', '/index.jsonopenapi', [
'headers' => ['Accept' => 'application/vnd.openapi+json'],
]);
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/vnd.openapi+json; charset=utf-8');
$this->assertJson($response->getContent());
$this->assertResponseStatusCodeSame(404);
}

public function testOpenApiSchemaWithNormalizationAttributes(): void
Expand Down
Loading