Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

added a JsonpHandler #266

Merged
merged 1 commit into from
@lsmith77
Owner

fixes #263

  • rebase
  • decide on how to recognize jsonp requests

The issue with recognizing jsonp requests is the following:
If users will just send application/javascript in their Accept headers, we will not know if they want js or jsonp since application/javascript will always be mapped back to just js. There are four solutions:

1) use application/javascript+jsonp or some similar mime type that is mapped exclusively to jsonp
2) simply assume that js means jsonp when the jsonp handler is active
3) require that controllers set jsonp explicitly as the format $view->setFormat('jsonp')
4) make the mime type configurable (currently implemented)

I should not that using 1) of course does not prevent someone form using 3). However in that case the Accept header would become irrelevant and also the content would still be served with Content-Type set as application/javascript+jsonp

DependencyInjection/Configuration.php
@@ -128,6 +128,13 @@ private function addViewSection(ArrayNodeDefinition $rootNode)
->end()
->end()
->scalarNode('failed_validation')->defaultValue(Codes::HTTP_BAD_REQUEST)->end()
+ ->arrayNode('jsonp_handler')
+ ->canBeUnset()
+ ->children()
+ ->scalarNode('callback_param')->defaultValue('callback')->end()
+ ->scalarNode('callback_filter')->defaultValue('/[a-z]+/i')->end()
@schmittjoh Owner

I would also allow numbers here. For example, jQuery sends this callback "jQuery171065827149929257_1343950463342".

@lsmith77 Owner
lsmith77 added a note

hmm ok i added numbers .. but it appears i also need to support underscores?

It would also be nice to allow periods to be used. The YUI library uses callback names that are stored within the global YUI variable - like YUI.Env.JSONP._12345 - http://yuilibrary.com/yui/docs/jsonp/#timing

@lsmith77 Owner
lsmith77 added a note

hmm .. well supporting dots too would essentially make the entire filter superfluous. but maybe the best approach for supporting YUI would be a dedicated optional pattern just for YUI.

@lsmith77 Owner
lsmith77 added a note

just to clarify .. this filter is for security

/[[:alnum:]-_.]+/i ?

@asm89 Owner
asm89 added a note

What should it be securing then? The callback is called on the implementers side, so I guess you're in trouble yourself if you do weird things with callbacks?

@lsmith77 Owner
lsmith77 added a note

i am not a frontend guy .. but yeah thinking about it .. the trouble is mostly on the frontend side having to trust the remote provider. i did a quick google and found a blog post describing similar intentions to what i have done here and people in the comments pointing out that it might not really be necessary:
http://tav.espians.com/sanitising-jsonp-callback-identifiers-for-security.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
DependencyInjection/FOSRestExtension.php
@@ -122,6 +124,21 @@ public function load(array $configs, ContainerBuilder $container)
$container->setParameter($this->getAlias().'.fallback_format', 'html');
}
+ if (!empty($config['view']['jsonp_handler'])) {
+ $handler = new DefinitionDecorator($config['service']['view_handler']);
+
+ $jsonpHandler = $container->getDefinition($this->getAlias().'.view_handler.jsonp');
+ $handler->addMethodCall('registerHandler', array('jsonp', array($jsonpHandler, 'createResponse')));
@stof Owner
stof added a note

why getting the definition to add it ? You should be using a reference instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@lsmith77
Owner

another topic to discuss is the mime type to register for jsonp

@arnaud-lb arnaud-lb commented on the diff
Resources/doc/2-the-view-layer.md
((26 lines not shown))
+ view:
+ jsonp_handler:
+ callback_param: mycallback
+ callback_filter: /^[a-z0-9_]+$/i
+```
+
+Finally the filter can also be disabled by setting it to false.
+
+```yaml
+# app/config/config.yml
+fos_rest:
+ view:
+ jsonp_handler:
+ callback_param: false
+```
+
@arnaud-lb Owner

Since jsonp allows non trusted sites to send requests on behalf of users and see the response, it should be noted that implicit authentication mechanisms must be disabled if jsonp is enabled (e.g. cookie or authorization header based auth).

The bundle could even disable these itself (or e.g. allow to configure a whitelist of auth providers, or remove cookies and authorization headers from the request) during jsonp requests.

I don't agree with that. You can make a jsonp request with some credentials into a private application.

@arnaud-lb Owner

You can make a jsonp request with some credentials.

But the credentials should not be sent implicitly by the browser, else any untrusted website can do an authenticated request without knowing the credentials, since the browser sends them implicitly.

So a jsonp API must not use cookies or http auth for authentication.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ilanco

@lsmith77 Hi, output of app/console container:debug | grep fos_rest.view_handler is empty, ouput of app/console container:debug | grep fos_rest is

fos_rest.body_listener                        container FOS\RestBundle\EventListener\BodyListener
fos_rest.decoder.json                         container FOS\Rest\Decoder\JsonDecoder
fos_rest.decoder.xml                          container FOS\Rest\Decoder\XmlDecoder
fos_rest.decoder_provider                     container FOS\RestBundle\Decoder\ContainerDecoderProvider
fos_rest.format_listener                      container FOS\RestBundle\EventListener\FormatListener
fos_rest.format_negotiator                    container FOS\Rest\Util\FormatNegotiator
fos_rest.mime_type_listener                   container FOS\RestBundle\EventListener\MimeTypeListener
fos_rest.request.param_fetcher                request   FOS\RestBundle\Request\ParamFetcher
fos_rest.request.param_fetcher.reader         container FOS\RestBundle\Request\ParamReader
fos_rest.router                               n/a       alias for router
fos_rest.routing.loader.controller            container FOS\RestBundle\Routing\Loader\RestRouteLoader20
fos_rest.routing.loader.processor             container FOS\RestBundle\Routing\Loader\RestRouteProcessor
fos_rest.routing.loader.reader.action         container FOS\RestBundle\Routing\Loader\Reader\RestActionReader
fos_rest.routing.loader.reader.controller     container FOS\RestBundle\Routing\Loader\Reader\RestControllerReader
fos_rest.routing.loader.xml_collection        container FOS\RestBundle\Routing\Loader\RestXmlCollectionLoader
fos_rest.routing.loader.yaml_collection       container FOS\RestBundle\Routing\Loader\RestYamlCollectionLoader
fos_rest.serializer                           n/a       alias for serializer
fos_rest.templating                           n/a       alias for templating
fos_rest.view_response_listener               container FOS\RestBundle\EventListener\ViewResponseListener
@lsmith77
Owner

@ilanco what version of Symfony are you using?

@ilanco

@lsmith77 symfony v2.0.16

@lsmith77
Owner

ok .. that must be the cause then .. i am on master.

@ilanco

@lsmith77 any chance in helping me getting it to work on 2.0.16 ?

@lsmith77
Owner

yeah .. looking into it ..

@lsmith77
Owner

i am sorry .. but i cannot reproduce this with a unit test and i dont have a symfony 2.0 project setup. i will not get to setting something up until next week. in the mean time if you want to dig in .. look at:
https://github.com/FriendsOfSymfony/FOSRestBundle/pull/266/files#L1R124

you will likely also need to look into the Container documentation. but essentially what i am doing there is making changes to the definition of the service (by default this is fos_rest.view_handler.default) and then overriding the previously created alias fos_rest.view_handler.

@Seldaek

@lsmith77 regarding the mime-type, application/javascript makes sense IMO, since you are basically serving JS.

@lsmith77
Owner

that unfortunately causes problems IIRC .. since the Request then decides that its a "js" format request.

@Seldaek

How does setting the response content-type do anything to the request?

@lsmith77
Owner

the issue is that i think the ViewHandler gets confused before .. i have to test it again, but it crapped out last time i used application/javascript

@travisbot

This pull request passes (merged 2c699918 into c05cdaa).

@travisbot

This pull request passes (merged 0738bad into 63cfa48).

@lsmith77
Owner

dont really have time to wrap this up .. i have noted the open issues i am aware of in the description

@lsmith77
Owner

a potential solution to the mime type question came up in #320. jsonp could be automatically be applied if a callback function is defined in the request. not sure i like this though ..

@Seldaek

Having jsonp support can be a security issue in some way if you don't know about it, so it's not ok to have it on by default. Having it configured globally would be a good first step, but ideally this should be an option/flag you set on the view allowing jsonp fallback. Whether it's done by detecting callback or with a .jsonp request format I don't really mind.

@lsmith77
Owner

it will be optional of course. i dont really want to add this to the View class, since i want to keep it format agnostic. That being said, maybe a a separate response listener that is configured on a per controller action basis is the way to go after all.

@jonathaningram

@lsmith77 any chance you'd be able to rebase this branch to pull in the latest commits from master?

@lsmith77
Owner

@jonathaningram rebased, as you can see above i am still not really happy with the implementation.

@jonathaningram

@lsmith77 thanks so much for doing that! I'm not sure if I can do anything to help, but please let me know?

Also, I have the same problem as @ilanco whereby we get the error You have requested a non-existent servicefos_rest.view_handler(i.e. anyfos_rest.view_handler.*service disappears entirely from the compiled container!) . I know you said that you tried figuring it out and writing a test to trap the issue, but it's really weird: searching the entire cache folder forview_handler` yields only two results, namely the two callback parameters:

'fos_rest.view_handler.jsonp.callback_param' => 'callback',
'fos_rest.view_handler.jsonp.callback_filter' => '/(^[a-z0-9_]+$)|(^YUI\\.Env\\.JSONP\\._[0-9]+$)/i',

(There is no default or jsonp fos_rest.view_handler). Force clearing the cache, restarting Apache doesn't help.

I thought maybe changing the fos_rest.view_handler.* services in view.xml from private to public would help, but it didn't seem to help.

It would seem there's an issue with the container definition decorator in the FOSRestExtension class, but again, changing/commenting-out different parts within https://github.com/FriendsOfSymfony/FOSRestBundle/pull/266/files#L1R123 didn't help either. I've compared your code with some other definition decorators in my vendors, and I can't figure out the issue either. Maybe someone with a bit more knowledge of compiling the container could take a look at that if block and shed insight if there's some secret problem in there that causes the code to not work pre Symfony dev-master?

I am on Symfony v2.1.5-dev (not dev-master) and I just updated FOSRestBundle to the rebased version.

@lsmith77
Owner

@jonathaningram i am sorry but i really dont have time to dig into this issue more. maybe you can get some assistance in debugging this from people on #symfony-dev IRC

@blaugueux blaugueux commented on the diff
View/JsonpHandler.php
((51 lines not shown))
+ * @param ViewHandler $handler
+ * @param View $view
+ * @param Request $request
+ * @param string $format
+ *
+ * @return Response
+ */
+ public function createResponse(ViewHandler $handler, View $view, Request $request, $format)
+ {
+ $response = $handler->createResponse($view, $request, 'json');
+
+ if ($response->isSuccessful()) {
+ $callback = $this->getCallback($request);
+ $response->setContent($callback.'('.$response->getContent().')');
+ $response->headers->set('Content-Type', $request->getMimeType($format));
+ }
@stof Owner
stof added a note

2 reasons:

  • this PR is 9 months old, so before the method was implemented in Symfony
  • the JsonResponse is intended to be used by passing the data and letting the JsonResponse class do the json_encode call internally. But the view handler is already serializing the data as json so it woould encode the json string as json again if you use the JsonResponse class, which is wrong.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@diegotdai

I wonder if this will ever make into master.

@lsmith77
Owner
@hd-deman

+1 for jsonp

DependencyInjection/FOSRestExtension.php
@@ -117,6 +120,21 @@ public function load(array $configs, ContainerBuilder $container)
$container->setParameter($this->getAlias().'.fallback_format', 'html');
}
+ if (!empty($config['view']['jsonp_handler'])) {
+ $handler = new DefinitionDecorator($config['service']['view_handler']);
+
+ $jsonpHandler = new Reference($this->getAlias().'.view_handler.jsonp');
+ $handler->addMethodCall('registerHandler', array('jsonp', array($jsonpHandler, 'createResponse')));
+ $container->setDefinition($this->getAlias().'.view_handler', $handler);
+
+ $container->setParameter($this->getAlias().'.view_handler.jsonp.callback_param', $config['view']['jsonp_handler']['callback_param']);
+ $container->setParameter($this->getAlias().'.view_handler.jsonp.callback_filter', $config['view']['jsonp_handler']['callback_filter']);
+
+ if (empty($config['view']['mime_types']['jsonp'])) {
+ $config['view']['mime_types']['jsonp'] = 'application/javascript+jsonp';
@lsmith77 Owner

Sure .. but it doesn't work. See #266 (comment)

@willdurand Owner

Not sure to understand the issue..

@lsmith77 Owner

Well the point of my tweet was that someone needs to figure this out. The comments in the ticket I made show that there was an issue. So would be great if you could try and determine a viable mime type and then we can wrap this PR up :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@lsmith77 lsmith77 merged commit e87f3e7 into from
@lsmith77 lsmith77 deleted the branch
@willdurand
Owner

I thought it didnt work, what did you do?

@lsmith77
Owner

See the ticket description. Also see liip/LiipHelloBundle@edb81a2#L0R155

@hd-deman

Hm. i just checkout master of rest-bundle. my config.yml like this:

fos_rest:
    view:
#        default_engine: php
        view_response_listener: force
        force_redirects:
          html: true
#          xml: true
        formats:
            jsonp: true
            json: true
            xml: true
            rss: false
        templating_formats:
            html: true
        mime_types:
            json: ['application/json', 'application/x-json', 'application/vnd.example-com.foo+json']
            jsonp: ['application/jsonp']
            rss: 'application/rss+xml'
            jpg: 'image/jpeg'
            png: 'image/png'
        jsonp_handler:
            callback_param: mycallback
            callback_filter: /^[a-z0-9_]+$/i
    body_listener: true
    param_fetcher_listener: force
    allowed_methods_listener: true
    access_denied_listener:
        json: true
    format_listener:
        default_priorities:
            - html
            - json
            - xml
            - rss
            - "*/*"
            # never reached due to "*/*"
            - ding
        # the subsequent line overrides the following line, so the default is NULL
        fallback_format: json
        fallback_format: ~
        prefer_extension: true
    routing_loader:
        default_format: ~
    exception:
        codes:
            'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404
            'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT
        messages:
            'Symfony\Component\Routing\Exception\ResourceNotFoundException': true
    service:
        view_handler: api.view_handler
    serializer:
#        version: 1.0
#        groups: [data]

When i try to request i get an error:

You have requested a non-existent service "fos_rest.view_handler".
also

php app/console container:debug | grep fos_rest.view_handler     

return nothing.
if i change vendor/friendsofsymfony/rest-bundle/FOS/RestBundle/Resources/config/view.xml from

<service id="fos_rest.view_handler.default" class="FOS\RestBundle\View\ViewHandler" public="false">

to

<service id="fos_rest.view_handler.default" class="FOS\RestBundle\View\ViewHandler" public="true">

then everything works as intended.

@lsmith77
Owner

can you try #526
need to clear the cache.

@hd-deman
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 15, 2013
  1. @lsmith77

    added a JsonpHandler

    lsmith77 authored
This page is out of date. Refresh to see the latest.
View
8 DependencyInjection/Configuration.php
@@ -144,6 +144,14 @@ private function addViewSection(ArrayNodeDefinition $rootNode)
->scalarNode('failed_validation')->defaultValue(Codes::HTTP_BAD_REQUEST)->end()
->scalarNode('empty_content')->defaultValue(Codes::HTTP_NO_CONTENT)->end()
->booleanNode('serialize_null')->defaultFalse()->end()
+ ->arrayNode('jsonp_handler')
+ ->canBeUnset()
+ ->children()
+ ->scalarNode('callback_param')->defaultValue('callback')->end()
+ ->scalarNode('callback_filter')->defaultValue('/(^[a-z0-9_]+$)|(^YUI\.Env\.JSONP\._[0-9]+$)/i')->end()
+ ->scalarNode('mime_type')->defaultValue('application/javascript')->end()
+ ->end()
+ ->end()
->end()
->end()
->end();
View
18 DependencyInjection/FOSRestExtension.php
@@ -15,6 +15,9 @@
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
+use Symfony\Component\DependencyInjection\DefinitionDecorator;
+use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\Kernel;
use FOS\Rest\Util\Codes;
@@ -130,6 +133,21 @@ public function load(array $configs, ContainerBuilder $container)
$container->setParameter($this->getAlias().'.fallback_format', 'html');
}
+ if (!empty($config['view']['jsonp_handler'])) {
+ $handler = new DefinitionDecorator($config['service']['view_handler']);
+
+ $jsonpHandler = new Reference($this->getAlias().'.view_handler.jsonp');
+ $handler->addMethodCall('registerHandler', array('jsonp', array($jsonpHandler, 'createResponse')));
+ $container->setDefinition($this->getAlias().'.view_handler', $handler);
+
+ $container->setParameter($this->getAlias().'.view_handler.jsonp.callback_param', $config['view']['jsonp_handler']['callback_param']);
+ $container->setParameter($this->getAlias().'.view_handler.jsonp.callback_filter', $config['view']['jsonp_handler']['callback_filter']);
+
+ if (empty($config['view']['mime_types']['jsonp'])) {
+ $config['view']['mime_types']['jsonp'] = $config['view']['jsonp_handler']['mime_type'];
+ }
+ }
+
if (!empty($config['view']['mime_types'])) {
$loader->load('mime_type_listener.xml');
View
7 Resources/config/view.xml
@@ -7,6 +7,8 @@
<parameters>
<parameter key="fos_rest.serializer.exclusion_strategy.version" />
<parameter key="fos_rest.serializer.exclusion_strategy.groups"/>
+ <parameter key="fos_rest.view_handler.jsonp.callback_param"/>
+ <parameter key="fos_rest.view_handler.jsonp.callback_filter"/>
</parameters>
<services>
@@ -23,5 +25,10 @@
</call>
</service>
+ <service id="fos_rest.view_handler.jsonp" class="FOS\RestBundle\View\JsonpHandler" public="false">
+ <argument>%fos_rest.view_handler.jsonp.callback_param%</argument>
+ <argument>%fos_rest.view_handler.jsonp.callback_filter%</argument>
+ </service>
+
</services>
</container>
View
38 Resources/doc/2-the-view-layer.md
@@ -214,5 +214,43 @@ class UsersController extends Controller
}
```
+#### Jsonp custom handler
+
+To enable the common use case of creating Jsonp responses this Bundle provides an
+easy solution to handle a custom handler for this use case. Enabling this setting
+also automatically uses the mime type listener (see the next chapter) to register
+a mime type for Jsonp.
+
+Simply add the following to your configuration
+
+```yaml
+# app/config/config.yml
+fos_rest:
+ view:
+ jsonp_handler: ~
+```
+
+It is also possible to customize both the name of the GET parameter with the callback,
+as well as the filter pattern that validates if the provided callback is valid or not.
+
+```yaml
+# app/config/config.yml
+fos_rest:
+ view:
+ jsonp_handler:
+ callback_param: mycallback
+ callback_filter: /^[a-z0-9_]+$/i
+```
+
+Finally the filter can also be disabled by setting it to false.
+
+```yaml
+# app/config/config.yml
+fos_rest:
+ view:
+ jsonp_handler:
+ callback_param: false
+```
+
@arnaud-lb Owner

Since jsonp allows non trusted sites to send requests on behalf of users and see the response, it should be noted that implicit authentication mechanisms must be disabled if jsonp is enabled (e.g. cookie or authorization header based auth).

The bundle could even disable these itself (or e.g. allow to configure a whitelist of auth providers, or remove cookies and authorization headers from the request) during jsonp requests.

I don't agree with that. You can make a jsonp request with some credentials into a private application.

@arnaud-lb Owner

You can make a jsonp request with some credentials.

But the credentials should not be sent implicitly by the browser, else any untrusted website can do an authenticated request without knowing the credentials, since the browser sends them implicitly.

So a jsonp API must not use cookies or http auth for authentication.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
## That was it!
[Return to the index](index.md) or continue reading about [Listener support](3-listener-support.md).
View
4 Resources/doc/configuration-reference.md
@@ -44,6 +44,10 @@ fos_rest:
failed_validation: 400
empty_content: 204
serialize_null: false
+ jsonp_handler:
+ callback_param: callback
+ callback_filter: /(^[a-z0-9_]+$)|(^YUI\.Env\.JSONP\._[0-9]+$)/i
+ mime_type: application/javascript
exception:
codes:
View
10 Tests/DependencyInjection/FOSRestExtensionTest.php
@@ -260,4 +260,14 @@ private function assertParameter($value, $key)
{
$this->assertEquals($value, $this->container->getParameter($key), sprintf('%s parameter is correct', $key));
}
+
+ public function testCheckViewHandlerWithJsonp()
+ {
+ $this->extension->load(array('fos_rest' => array('view' => array('jsonp_handler' => null))), $this->container);
+
+ $this->assertTrue($this->container->has('fos_rest.view_handler'));
+
+ $viewHandler = $this->container->getDefinition('fos_rest.view_handler');
+ $this->assertInstanceOf('Symfony\Component\DependencyInjection\DefinitionDecorator', $viewHandler);
+ }
}
View
127 Tests/View/JsonpHandlerTest.php
@@ -0,0 +1,127 @@
+<?php
+
+/*
+ * This file is part of the FOSRestBundle package.
+ *
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\RestBundle\Tests\View;
+
+use FOS\RestBundle\View\View;
+use FOS\RestBundle\View\ViewHandler;
+use FOS\RestBundle\View\JsonpHandler;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Jsonp handler test
+ *
+ * @author Victor Berchet <victor@suumit.com>
+ * @author Lukas K. Smith <smith@pooteeweet.org>
+ */
+class JsonpHandlerTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider handleDataProvider
+ */
+ public function testHandle($query, $callbackFilter = '/(^[a-z0-9_]+$)|(^YUI\.Env\.JSONP\._[0-9]+$)/i')
+ {
+ $data = array('foo' => 'bar');
+
+ $viewHandler = new ViewHandler(array('jsonp' => false));
+ $jsonpHandler = new JsonpHandler(key($query), $callbackFilter);
+ $viewHandler->registerHandler('jsonp', array($jsonpHandler, 'createResponse'));
+
+ $container = $this->getMock('\Symfony\Component\DependencyInjection\Container', array('get', 'getParameter'));
+ $serializer = $this->getMock('\stdClass', array('serialize', 'setVersion'));
+ $serializer
+ ->expects($this->once())
+ ->method('serialize')
+ ->will($this->returnValue(var_export($data, true)));
+
+ $container
+ ->expects($this->once())
+ ->method('get')
+ ->with('fos_rest.serializer')
+ ->will($this->returnValue($serializer));
+
+ $container
+ ->expects($this->any())
+ ->method('getParameter')
+ ->will($this->onConsecutiveCalls('version', '1.0'));
+
+ $viewHandler->setContainer($container);
+
+ $view = new View($data);
+ $view->setFormat('jsonp');
+ $request = new Request($query);
+
+ $response = $viewHandler->handle($view, $request);
+
+ $this->assertEquals(reset($query).'('.var_export($data, true).')', $response->getContent());
+ }
+
+ public static function handleDataProvider()
+ {
+ return array(
+ 'jQuery callback syntax' => array(array('callback' => 'jQuery171065827149929257_1343950463342')),
+ 'YUI callback syntax' => array(array('callback' => 'YUI.Env.JSONP._12345')),
+ 'custom callback param' => array(array('custom' => '1234')),
+ 'custom callback filter' => array(array('custom' => '1234.asdas.122'), false),
+ );
+ }
+
+ /**
+ * @expectedException \Symfony\Component\HttpKernel\Exception\HttpException
+ * @dataProvider getCallbackFailureDataProvider
+ */
+ public function testGetCallbackFailure(Request $request, $callbackFilter = '/(^[a-z0-9_]+$)|(^YUI\.Env\.JSONP\._[0-9]+$)/i')
+ {
+ $data = array('foo' => 'bar');
+
+ $viewHandler = new ViewHandler(array('jsonp' => false));
+ $jsonpHandler = new JsonpHandler('callback', $callbackFilter);
+ $viewHandler->registerHandler('jsonp', array($jsonpHandler, 'createResponse'));
+
+ $container = $this->getMock('\Symfony\Component\DependencyInjection\Container', array('get', 'getParameter'));
+ $serializer = $this->getMock('\stdClass', array('serialize', 'setVersion'));
+ $serializer
+ ->expects($this->once())
+ ->method('serialize')
+ ->will($this->returnValue(var_export($data, true)));
+
+ $container
+ ->expects($this->once())
+ ->method('get')
+ ->with('fos_rest.serializer')
+ ->will($this->returnValue($serializer));
+
+ $container
+ ->expects($this->any())
+ ->method('getParameter')
+ ->will($this->onConsecutiveCalls('version', '1.0'));
+
+ $viewHandler->setContainer($container);
+
+ $data = array('foo' => 'bar');
+
+ $view = new View($data);
+ $view->setFormat('jsonp');
+ $viewHandler->handle($view, $request);
+ }
+
+ public function getCallbackFailureDataProvider()
+ {
+ return array(
+ 'no callback' => array(new Request()),
+ 'incorrect callback param name' => array(new Request(array('foo' => 'bar'))),
+ 'incorrect callback param value' => array(new Request(array('callback' => 'ding.dong'))),
+ 'incorrect callback param name and value' => array(new Request(array('foo' => 'bar'))),
+ 'incorrect callback param value with a custom filter' => array(new Request(array('foo' => 'bar')), '/[0-9]+/'),
+ );
+ }
+}
View
70 View/JsonpHandler.php
@@ -0,0 +1,70 @@
+<?php
+
+/*
+ * This file is part of the FOSRestBundle package.
+ *
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\RestBundle\View;
+
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+
+use FOS\Rest\Util\Codes;
+
+/**
+ * Implements a custom handler for JSONP leveraging the ViewHandler
+ *
+ * @author Lukas K. Smith <smith@pooteeweet.org>
+ */
+class JsonpHandler
+{
+ protected $callbackParam;
+ protected $callbackFilter;
+
+ public function __construct($callbackParam, $callbackFilter)
+ {
+ $this->callbackParam = $callbackParam;
+ $this->callbackFilter = $callbackFilter;
+ }
+
+ protected function getCallback(Request $request)
+ {
+ $callback = $request->query->get($this->callbackParam);
+
+ if ($this->callbackFilter && !preg_match($this->callbackFilter, $callback)) {
+ $msg = "Callback '$callback' does not match '{$this->callbackFilter}'";
+ throw new HttpException(Codes::HTTP_BAD_REQUEST, $msg);
+ }
+
+ return $callback;
+ }
+
+ /**
+ * Handles wrapping a JSON response into a JSONP response
+ *
+ * @param ViewHandler $handler
+ * @param View $view
+ * @param Request $request
+ * @param string $format
+ *
+ * @return Response
+ */
+ public function createResponse(ViewHandler $handler, View $view, Request $request, $format)
+ {
+ $response = $handler->createResponse($view, $request, 'json');
+
+ if ($response->isSuccessful()) {
+ $callback = $this->getCallback($request);
+ $response->setContent($callback.'('.$response->getContent().')');
+ $response->headers->set('Content-Type', $request->getMimeType($format));
+ }
@stof Owner
stof added a note

2 reasons:

  • this PR is 9 months old, so before the method was implemented in Symfony
  • the JsonResponse is intended to be used by passing the data and letting the JsonResponse class do the json_encode call internally. But the view handler is already serializing the data as json so it woould encode the json string as json again if you use the JsonResponse class, which is wrong.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+ return $response;
+ }
+}
Something went wrong with that request. Please try again.