Skip to content

Commit e87f3e7

Browse files
committed
Merge pull request #266 from FriendsOfSymfony/jsonp_handler
added a JsonpHandler
2 parents 133b816 + 383e93c commit e87f3e7

File tree

8 files changed

+282
-0
lines changed

8 files changed

+282
-0
lines changed

DependencyInjection/Configuration.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ private function addViewSection(ArrayNodeDefinition $rootNode)
144144
->scalarNode('failed_validation')->defaultValue(Codes::HTTP_BAD_REQUEST)->end()
145145
->scalarNode('empty_content')->defaultValue(Codes::HTTP_NO_CONTENT)->end()
146146
->booleanNode('serialize_null')->defaultFalse()->end()
147+
->arrayNode('jsonp_handler')
148+
->canBeUnset()
149+
->children()
150+
->scalarNode('callback_param')->defaultValue('callback')->end()
151+
->scalarNode('callback_filter')->defaultValue('/(^[a-z0-9_]+$)|(^YUI\.Env\.JSONP\._[0-9]+$)/i')->end()
152+
->scalarNode('mime_type')->defaultValue('application/javascript')->end()
153+
->end()
154+
->end()
147155
->end()
148156
->end()
149157
->end();

DependencyInjection/FOSRestExtension.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
1717
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
18+
use Symfony\Component\DependencyInjection\DefinitionDecorator;
19+
use Symfony\Component\DependencyInjection\Definition;
20+
use Symfony\Component\DependencyInjection\Reference;
1821
use Symfony\Component\HttpKernel\Kernel;
1922

2023
use FOS\Rest\Util\Codes;
@@ -130,6 +133,21 @@ public function load(array $configs, ContainerBuilder $container)
130133
$container->setParameter($this->getAlias().'.fallback_format', 'html');
131134
}
132135

136+
if (!empty($config['view']['jsonp_handler'])) {
137+
$handler = new DefinitionDecorator($config['service']['view_handler']);
138+
139+
$jsonpHandler = new Reference($this->getAlias().'.view_handler.jsonp');
140+
$handler->addMethodCall('registerHandler', array('jsonp', array($jsonpHandler, 'createResponse')));
141+
$container->setDefinition($this->getAlias().'.view_handler', $handler);
142+
143+
$container->setParameter($this->getAlias().'.view_handler.jsonp.callback_param', $config['view']['jsonp_handler']['callback_param']);
144+
$container->setParameter($this->getAlias().'.view_handler.jsonp.callback_filter', $config['view']['jsonp_handler']['callback_filter']);
145+
146+
if (empty($config['view']['mime_types']['jsonp'])) {
147+
$config['view']['mime_types']['jsonp'] = $config['view']['jsonp_handler']['mime_type'];
148+
}
149+
}
150+
133151
if (!empty($config['view']['mime_types'])) {
134152
$loader->load('mime_type_listener.xml');
135153

Resources/config/view.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
<parameters>
88
<parameter key="fos_rest.serializer.exclusion_strategy.version" />
99
<parameter key="fos_rest.serializer.exclusion_strategy.groups"/>
10+
<parameter key="fos_rest.view_handler.jsonp.callback_param"/>
11+
<parameter key="fos_rest.view_handler.jsonp.callback_filter"/>
1012
</parameters>
1113

1214
<services>
@@ -23,5 +25,10 @@
2325
</call>
2426
</service>
2527

28+
<service id="fos_rest.view_handler.jsonp" class="FOS\RestBundle\View\JsonpHandler" public="false">
29+
<argument>%fos_rest.view_handler.jsonp.callback_param%</argument>
30+
<argument>%fos_rest.view_handler.jsonp.callback_filter%</argument>
31+
</service>
32+
2633
</services>
2734
</container>

Resources/doc/2-the-view-layer.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,5 +214,43 @@ class UsersController extends Controller
214214
}
215215
```
216216

217+
#### Jsonp custom handler
218+
219+
To enable the common use case of creating Jsonp responses this Bundle provides an
220+
easy solution to handle a custom handler for this use case. Enabling this setting
221+
also automatically uses the mime type listener (see the next chapter) to register
222+
a mime type for Jsonp.
223+
224+
Simply add the following to your configuration
225+
226+
```yaml
227+
# app/config/config.yml
228+
fos_rest:
229+
view:
230+
jsonp_handler: ~
231+
```
232+
233+
It is also possible to customize both the name of the GET parameter with the callback,
234+
as well as the filter pattern that validates if the provided callback is valid or not.
235+
236+
```yaml
237+
# app/config/config.yml
238+
fos_rest:
239+
view:
240+
jsonp_handler:
241+
callback_param: mycallback
242+
callback_filter: /^[a-z0-9_]+$/i
243+
```
244+
245+
Finally the filter can also be disabled by setting it to false.
246+
247+
```yaml
248+
# app/config/config.yml
249+
fos_rest:
250+
view:
251+
jsonp_handler:
252+
callback_param: false
253+
```
254+
217255
## That was it!
218256
[Return to the index](index.md) or continue reading about [Listener support](3-listener-support.md).

Resources/doc/configuration-reference.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ fos_rest:
4444
failed_validation: 400
4545
empty_content: 204
4646
serialize_null: false
47+
jsonp_handler:
48+
callback_param: callback
49+
callback_filter: /(^[a-z0-9_]+$)|(^YUI\.Env\.JSONP\._[0-9]+$)/i
50+
mime_type: application/javascript
4751
exception:
4852
codes:
4953

Tests/DependencyInjection/FOSRestExtensionTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,4 +260,14 @@ private function assertParameter($value, $key)
260260
{
261261
$this->assertEquals($value, $this->container->getParameter($key), sprintf('%s parameter is correct', $key));
262262
}
263+
264+
public function testCheckViewHandlerWithJsonp()
265+
{
266+
$this->extension->load(array('fos_rest' => array('view' => array('jsonp_handler' => null))), $this->container);
267+
268+
$this->assertTrue($this->container->has('fos_rest.view_handler'));
269+
270+
$viewHandler = $this->container->getDefinition('fos_rest.view_handler');
271+
$this->assertInstanceOf('Symfony\Component\DependencyInjection\DefinitionDecorator', $viewHandler);
272+
}
263273
}

Tests/View/JsonpHandlerTest.php

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSRestBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\RestBundle\Tests\View;
13+
14+
use FOS\RestBundle\View\View;
15+
use FOS\RestBundle\View\ViewHandler;
16+
use FOS\RestBundle\View\JsonpHandler;
17+
18+
use Symfony\Component\HttpFoundation\Request;
19+
20+
/**
21+
* Jsonp handler test
22+
*
23+
* @author Victor Berchet <[email protected]>
24+
* @author Lukas K. Smith <[email protected]>
25+
*/
26+
class JsonpHandlerTest extends \PHPUnit_Framework_TestCase
27+
{
28+
/**
29+
* @dataProvider handleDataProvider
30+
*/
31+
public function testHandle($query, $callbackFilter = '/(^[a-z0-9_]+$)|(^YUI\.Env\.JSONP\._[0-9]+$)/i')
32+
{
33+
$data = array('foo' => 'bar');
34+
35+
$viewHandler = new ViewHandler(array('jsonp' => false));
36+
$jsonpHandler = new JsonpHandler(key($query), $callbackFilter);
37+
$viewHandler->registerHandler('jsonp', array($jsonpHandler, 'createResponse'));
38+
39+
$container = $this->getMock('\Symfony\Component\DependencyInjection\Container', array('get', 'getParameter'));
40+
$serializer = $this->getMock('\stdClass', array('serialize', 'setVersion'));
41+
$serializer
42+
->expects($this->once())
43+
->method('serialize')
44+
->will($this->returnValue(var_export($data, true)));
45+
46+
$container
47+
->expects($this->once())
48+
->method('get')
49+
->with('fos_rest.serializer')
50+
->will($this->returnValue($serializer));
51+
52+
$container
53+
->expects($this->any())
54+
->method('getParameter')
55+
->will($this->onConsecutiveCalls('version', '1.0'));
56+
57+
$viewHandler->setContainer($container);
58+
59+
$view = new View($data);
60+
$view->setFormat('jsonp');
61+
$request = new Request($query);
62+
63+
$response = $viewHandler->handle($view, $request);
64+
65+
$this->assertEquals(reset($query).'('.var_export($data, true).')', $response->getContent());
66+
}
67+
68+
public static function handleDataProvider()
69+
{
70+
return array(
71+
'jQuery callback syntax' => array(array('callback' => 'jQuery171065827149929257_1343950463342')),
72+
'YUI callback syntax' => array(array('callback' => 'YUI.Env.JSONP._12345')),
73+
'custom callback param' => array(array('custom' => '1234')),
74+
'custom callback filter' => array(array('custom' => '1234.asdas.122'), false),
75+
);
76+
}
77+
78+
/**
79+
* @expectedException \Symfony\Component\HttpKernel\Exception\HttpException
80+
* @dataProvider getCallbackFailureDataProvider
81+
*/
82+
public function testGetCallbackFailure(Request $request, $callbackFilter = '/(^[a-z0-9_]+$)|(^YUI\.Env\.JSONP\._[0-9]+$)/i')
83+
{
84+
$data = array('foo' => 'bar');
85+
86+
$viewHandler = new ViewHandler(array('jsonp' => false));
87+
$jsonpHandler = new JsonpHandler('callback', $callbackFilter);
88+
$viewHandler->registerHandler('jsonp', array($jsonpHandler, 'createResponse'));
89+
90+
$container = $this->getMock('\Symfony\Component\DependencyInjection\Container', array('get', 'getParameter'));
91+
$serializer = $this->getMock('\stdClass', array('serialize', 'setVersion'));
92+
$serializer
93+
->expects($this->once())
94+
->method('serialize')
95+
->will($this->returnValue(var_export($data, true)));
96+
97+
$container
98+
->expects($this->once())
99+
->method('get')
100+
->with('fos_rest.serializer')
101+
->will($this->returnValue($serializer));
102+
103+
$container
104+
->expects($this->any())
105+
->method('getParameter')
106+
->will($this->onConsecutiveCalls('version', '1.0'));
107+
108+
$viewHandler->setContainer($container);
109+
110+
$data = array('foo' => 'bar');
111+
112+
$view = new View($data);
113+
$view->setFormat('jsonp');
114+
$viewHandler->handle($view, $request);
115+
}
116+
117+
public function getCallbackFailureDataProvider()
118+
{
119+
return array(
120+
'no callback' => array(new Request()),
121+
'incorrect callback param name' => array(new Request(array('foo' => 'bar'))),
122+
'incorrect callback param value' => array(new Request(array('callback' => 'ding.dong'))),
123+
'incorrect callback param name and value' => array(new Request(array('foo' => 'bar'))),
124+
'incorrect callback param value with a custom filter' => array(new Request(array('foo' => 'bar')), '/[0-9]+/'),
125+
);
126+
}
127+
}

View/JsonpHandler.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSRestBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\RestBundle\View;
13+
14+
use Symfony\Component\HttpFoundation\Response;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Exception\HttpException;
17+
18+
use FOS\Rest\Util\Codes;
19+
20+
/**
21+
* Implements a custom handler for JSONP leveraging the ViewHandler
22+
*
23+
* @author Lukas K. Smith <[email protected]>
24+
*/
25+
class JsonpHandler
26+
{
27+
protected $callbackParam;
28+
protected $callbackFilter;
29+
30+
public function __construct($callbackParam, $callbackFilter)
31+
{
32+
$this->callbackParam = $callbackParam;
33+
$this->callbackFilter = $callbackFilter;
34+
}
35+
36+
protected function getCallback(Request $request)
37+
{
38+
$callback = $request->query->get($this->callbackParam);
39+
40+
if ($this->callbackFilter && !preg_match($this->callbackFilter, $callback)) {
41+
$msg = "Callback '$callback' does not match '{$this->callbackFilter}'";
42+
throw new HttpException(Codes::HTTP_BAD_REQUEST, $msg);
43+
}
44+
45+
return $callback;
46+
}
47+
48+
/**
49+
* Handles wrapping a JSON response into a JSONP response
50+
*
51+
* @param ViewHandler $handler
52+
* @param View $view
53+
* @param Request $request
54+
* @param string $format
55+
*
56+
* @return Response
57+
*/
58+
public function createResponse(ViewHandler $handler, View $view, Request $request, $format)
59+
{
60+
$response = $handler->createResponse($view, $request, 'json');
61+
62+
if ($response->isSuccessful()) {
63+
$callback = $this->getCallback($request);
64+
$response->setContent($callback.'('.$response->getContent().')');
65+
$response->headers->set('Content-Type', $request->getMimeType($format));
66+
}
67+
68+
return $response;
69+
}
70+
}

0 commit comments

Comments
 (0)