Skip to content

Commit

Permalink
Merge pull request #871 from romainneutron/csp-compatibility
Browse files Browse the repository at this point in the history
Make swagger UI compatible in a CSP environment
  • Loading branch information
dunglas committed Dec 13, 2016
2 parents 7fb1d50 + c0b93bd commit 59787ce
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 92 deletions.
46 changes: 25 additions & 21 deletions src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php
Expand Up @@ -16,7 +16,8 @@
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
* Displays the documentation in Swagger UI.
Expand All @@ -27,19 +28,21 @@ final class SwaggerUiAction
{
private $resourceNameCollectionFactory;
private $resourceMetadataFactory;
private $serializer;
private $normalizer;
private $twig;
private $urlGenerator;
private $title;
private $description;
private $version;
private $formats = [];

public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, SerializerInterface $serializer, \Twig_Environment $twig, string $title = '', string $description = '', string $version = '', array $formats = [])
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, \Twig_Environment $twig, UrlGeneratorInterface $urlGenerator, string $title = '', string $description = '', string $version = '', array $formats = [])
{
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->serializer = $serializer;
$this->normalizer = $normalizer;
$this->twig = $twig;
$this->urlGenerator = $urlGenerator;
$this->title = $title;
$this->description = $description;
$this->version = $version;
Expand All @@ -50,10 +53,7 @@ public function __invoke(Request $request)
{
$documentation = new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version, $this->formats);

return new Response($this->twig->render(
'ApiPlatformBundle:SwaggerUi:index.html.twig',
$this->getContext($request) + ['spec' => $this->serializer->serialize($documentation, 'json')])
);
return new Response($this->twig->render('ApiPlatformBundle:SwaggerUi:index.html.twig', $this->getContext($request, $documentation)));
}

/**
Expand All @@ -63,29 +63,33 @@ public function __invoke(Request $request)
*
* @return array
*/
private function getContext(Request $request): array
private function getContext(Request $request, Documentation $documentation): array
{
$context = [
'title' => $this->title,
'description' => $this->description,
'formats' => $this->formats,
'shortName' => null,
'operationId' => null,
];

if (!$request->isMethodSafe(false) || null === $resourceClass = $request->attributes->get('_api_resource_class')) {
return $context;
}
$swaggerData = [
'url' => $this->urlGenerator->generate('api_doc', ['format' => 'json']),
'spec' => $this->normalizer->normalize($documentation, 'json'),
];

if ($request->isMethodSafe(false) && null !== $resourceClass = $request->attributes->get('_api_resource_class')) {
$swaggerData['id'] = $request->attributes->get('id');
$swaggerData['queryParameters'] = $request->query->all();

$metadata = $this->resourceMetadataFactory->create($resourceClass);
$context['shortName'] = $metadata->getShortName();
$metadata = $this->resourceMetadataFactory->create($resourceClass);
$swaggerData['shortName'] = $metadata->getShortName();

if (null !== $collectionOperationName = $request->attributes->get('_api_collection_operation_name')) {
$context['operationId'] = sprintf('%s%sCollection', $collectionOperationName, $context['shortName']);
} elseif (null !== $itemOperationName = $request->attributes->get('_api_item_operation_name')) {
$context['operationId'] = sprintf('%s%sItem', $itemOperationName, $context['shortName']);
if (null !== $collectionOperationName = $request->attributes->get('_api_collection_operation_name')) {
$swaggerData['operationId'] = sprintf('%s%sCollection', $collectionOperationName, $swaggerData['shortName']);
} elseif (null !== $itemOperationName = $request->attributes->get('_api_item_operation_name')) {
$swaggerData['operationId'] = sprintf('%s%sItem', $itemOperationName, $swaggerData['shortName']);
}
}

return $context;
return $context + ['swagger_data' => $swaggerData];
}
}
3 changes: 2 additions & 1 deletion src/Bridge/Symfony/Bundle/Resources/config/swagger.xml
Expand Up @@ -38,14 +38,15 @@
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.serializer" />
<argument type="service" id="twig" />
<argument type="service" id="router" />
<argument>%api_platform.title%</argument>
<argument>%api_platform.description%</argument>
<argument>%api_platform.version%</argument>
<argument>%api_platform.formats%</argument>
<argument>%api_platform.title%</argument>
<argument>%api_platform.description%</argument>
</service>

</services>

</container>
50 changes: 50 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/public/init-swagger-ui.js
@@ -0,0 +1,50 @@
$(function () {
var data = JSON.parse($('#swagger-data').html());
window.swaggerUi = new SwaggerUi({
url: data.url,
spec: data.spec,
dom_id: 'swagger-ui-container',
supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
onComplete: function() {
$('pre code').each(function(i, e) {
hljs.highlightBlock(e)
});

if (data.operationId !== undefined) {
var queryParameters = data.queryParameters;
var domSelector = '#' + data.shortName+'_'+data.operationId;

$(domSelector + ' form.sandbox input.parameter').each(function (i, e) {
var $e = $(e);
var name = $e.attr('name');

if (name in queryParameters) {
$e.val(queryParameters[name]);
}
});

if (data.id) {
$(domSelector + ' form.sandbox input[name="id"]').val(data.id);
}

$(domSelector + ' form.sandbox').submit();
document.location.hash = '#!/' + data.shortName + '/' + data.operationId;
}
},
onFailure: function() {
log('Unable to Load SwaggerUI');
},
docExpansion: 'list',
jsonEditor: false,
defaultModelRendering: 'schema',
showRequestHeaders: true
});

window.swaggerUi.load();

function log() {
if ('console' in window) {
console.log.apply(console, arguments);
}
}
});
Expand Up @@ -23,59 +23,10 @@
<script src="{{ asset('bundles/apiplatform/swagger-ui/lib/jsoneditor.min.js') }}"></script>
<script src="{{ asset('bundles/apiplatform/swagger-ui/lib/marked.js') }}"></script>
<script src="{{ asset('bundles/apiplatform/swagger-ui/lib/swagger-oauth.js') }}"></script>
<script src="{{ asset('bundles/apiplatform/init-swagger-ui.js') }}"></script>

<script>
$(function () {
window.swaggerUi = new SwaggerUi({
url: '{{ path('api_doc', {'_format': 'json'} ) }}',
spec: {{ spec|replace({'<': '\u003c'})|raw }},
dom_id: 'swagger-ui-container',
supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
onComplete: function() {
$('pre code').each(function(i, e) {
hljs.highlightBlock(e)
});
{% if operationId is not null %}
{% set domId = shortName ~ '_' ~ operationId %}
{% set id = app.request.attributes.get('id') %}
var queryParameters = {{ app.request.query.all()|json_encode|replace({'<': '\u003c'})|raw }};
$('#{{ domId|escape('js') }} form.sandbox input.parameter').each(function (i, e) {
var $e = $(e);
var name = $e.attr('name');
if (name in queryParameters) {
$e.val(queryParameters[name]);
}
});
{% if id %}
$('#{{ domId|escape('js') }} form.sandbox input[name="id"]').val('{{ id|escape('js') }}');
{% endif %}
$('#{{ domId|escape('js') }} form.sandbox').submit();
document.location.hash = '#!/{{ shortName|escape('js') }}/{{ operationId|escape('js') }}';
{% endif %}
},
onFailure: function() {
log('Unable to Load SwaggerUI');
},
docExpansion: 'list',
jsonEditor: false,
defaultModelRendering: 'schema',
showRequestHeaders: true
});
window.swaggerUi.load();
function log() {
if ('console' in window) {
console.log.apply(console, arguments);
}
}
});
</script>
{# json_encode(65) is for JSON_UNESCAPED_SLASHES|JSON_HEX_TAG to avoid JS XSS #}
<script id="swagger-data" type="application/json">{{ swagger_data|json_encode(65)|raw }}</script>
</head>

<body class="swagger-section">
Expand Down
56 changes: 38 additions & 18 deletions tests/Bridge/Symfony/Bundle/Action/SwaggerUiActionTest.php
Expand Up @@ -20,7 +20,8 @@
use Prophecy\Argument;
use Prophecy\Prophecy\ProphecyInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Routing\Generator\UrlGenerator;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

/**
* @author Kévin Dunglas <dunglas@gmail.com>
Expand All @@ -38,14 +39,18 @@ public function testInvoke(Request $request, ProphecyInterface $twigProphecy)
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
$resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadata('F'))->shouldBeCalled();

$serializerProphecy = $this->prophesize(SerializerInterface::class);
$serializerProphecy->serialize(Argument::type(Documentation::class), 'json')->willReturn('hello')->shouldBeCalled();
$normalizerProphecy = $this->prophesize(NormalizerInterface::class);
$normalizerProphecy->normalize(Argument::type(Documentation::class), 'json')->willReturn(['Hello' => 'world'])->shouldBeCalled();

$urlGeneratorProphecy = $this->prophesize(UrlGenerator::class);
$urlGeneratorProphecy->generate('api_doc', ['format' => 'json'])->willReturn('/url')->shouldBeCalled();

$action = new SwaggerUiAction(
$resourceNameCollectionFactoryProphecy->reveal(),
$resourceMetadataFactoryProphecy->reveal(),
$serializerProphecy->reveal(),
$twigProphecy->reveal()
$normalizerProphecy->reveal(),
$twigProphecy->reveal(),
$urlGeneratorProphecy->reveal()
);
$action($request);
}
Expand All @@ -57,22 +62,32 @@ public function getInvokeParameters()

$twigCollectionProphecy = $this->prophesize(\Twig_Environment::class);
$twigCollectionProphecy->render('ApiPlatformBundle:SwaggerUi:index.html.twig', [
'spec' => 'hello',
'shortName' => 'F',
'operationId' => 'getFCollection',
'title' => '',
'description' => '',
'formats' => [],
'swagger_data' => [
'url' => '/url',
'spec' => ['Hello' => 'world'],
'shortName' => 'F',
'operationId' => 'getFCollection',
'id' => null,
'queryParameters' => [],
],
])->shouldBeCalled();

$twigItemProphecy = $this->prophesize(\Twig_Environment::class);
$twigItemProphecy->render('ApiPlatformBundle:SwaggerUi:index.html.twig', [
'spec' => 'hello',
'shortName' => 'F',
'operationId' => 'getFItem',
'title' => '',
'description' => '',
'formats' => [],
'swagger_data' => [
'url' => '/url',
'spec' => ['Hello' => 'world'],
'shortName' => 'F',
'operationId' => 'getFItem',
'id' => null,
'queryParameters' => [],
],
])->shouldBeCalled();

return [
Expand All @@ -90,24 +105,29 @@ public function testDoNotRunCurrentRequest(Request $request)
$resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection(['Foo', 'Bar']))->shouldBeCalled();

$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
$serializerProphecy = $this->prophesize(SerializerInterface::class);
$serializerProphecy->serialize(Argument::type(Documentation::class), 'json')->willReturn('hello')->shouldBeCalled();
$normalizerProphecy = $this->prophesize(NormalizerInterface::class);
$normalizerProphecy->normalize(Argument::type(Documentation::class), 'json')->willReturn(['Hello' => 'world'])->shouldBeCalled();

$twigProphecy = $this->prophesize(\Twig_Environment::class);
$twigProphecy->render('ApiPlatformBundle:SwaggerUi:index.html.twig', [
'spec' => 'hello',
'shortName' => null,
'operationId' => null,
'title' => '',
'description' => '',
'formats' => [],
'swagger_data' => [
'url' => '/url',
'spec' => ['Hello' => 'world'],
],
])->shouldBeCalled();

$urlGeneratorProphecy = $this->prophesize(UrlGenerator::class);
$urlGeneratorProphecy->generate('api_doc', ['format' => 'json'])->willReturn('/url')->shouldBeCalled();

$action = new SwaggerUiAction(
$resourceNameCollectionFactoryProphecy->reveal(),
$resourceMetadataFactoryProphecy->reveal(),
$serializerProphecy->reveal(),
$twigProphecy->reveal()
$normalizerProphecy->reveal(),
$twigProphecy->reveal(),
$urlGeneratorProphecy->reveal()
);
$action($request);
}
Expand Down

0 comments on commit 59787ce

Please sign in to comment.