├── .gitignore
├── Tests
├── Fixtures
│ ├── BazingaRestExtraTestBundle.php
│ ├── Controller
│ │ ├── TestInvokeController.php
│ │ ├── TestInvokeCsrfController.php
│ │ ├── TestCsrfController.php
│ │ └── TestController.php
│ ├── Model
│ │ └── Test.php
│ └── app
│ │ ├── config
│ │ ├── symfony-3
│ │ │ ├── default.yml
│ │ │ └── routing.yml
│ │ └── symfony-4
│ │ │ ├── default.yml
│ │ │ └── routing.yml
│ │ └── AppKernel.php
├── WebTestCase.php
├── bootstrap.php
└── EventListener
│ ├── LinkRequestListenerTest.php
│ └── CsrfDoubleSubmitListenerTest.php
├── BazingaRestExtraBundle.php
├── UPGRADE-2.0.md
├── Annotation
└── CsrfDoubleSubmit.php
├── Model
└── LinkHeader.php
├── Resources
├── config
│ ├── link_request_listener.xml
│ └── csrf_double_submit_listener.xml
├── meta
│ └── LICENSE
└── doc
│ └── index.md
├── README.md
├── composer.json
├── CONTRIBUTING.md
├── phpunit.xml.dist
├── Test
└── WebTestCase.php
├── DependencyInjection
├── BazingaRestExtraExtension.php
└── Configuration.php
├── .travis.yml
└── EventListener
├── CsrfDoubleSubmitListener.php
└── LinkRequestListener.php
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | composer.lock
3 |
--------------------------------------------------------------------------------
/Tests/Fixtures/BazingaRestExtraTestBundle.php:
--------------------------------------------------------------------------------
1 | id = $id;
12 | }
13 |
14 | public function getId()
15 | {
16 | return $this->id;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/BazingaRestExtraBundle.php:
--------------------------------------------------------------------------------
1 | url = trim($url);
24 | $this->rel = trim($rel);
25 | }
26 |
27 | /**
28 | * @return string
29 | */
30 | public function getRel()
31 | {
32 | return $this->rel;
33 | }
34 |
35 | /**
36 | * @return boolean
37 | */
38 | public function hasRel()
39 | {
40 | return !empty($this->rel);
41 | }
42 |
43 | /**
44 | * @return string
45 | */
46 | public function getUrl()
47 | {
48 | return $this->url;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Resources/config/link_request_listener.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | Bazinga\Bundle\RestExtraBundle\EventListener\LinkRequestListener
8 |
9 |
10 |
11 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | BazingaRestExtraBundle
2 | ======================
3 |
4 | [](http://travis-ci.org/willdurand/BazingaRestExtraBundle)
5 |
6 | This bundle provides extra features for your REST APIs built using Symfony.
7 |
8 |
9 | Documentation
10 | -------------
11 |
12 | For documentation, see:
13 |
14 | Resources/doc/
15 |
16 | [Read the documentation](https://github.com/willdurand/BazingaRestExtraBundle/blob/master/Resources/doc/index.md)
17 |
18 |
19 | Contributing
20 | ------------
21 |
22 | See
23 | [CONTRIBUTING](https://github.com/willdurand/BazingaRestExtraBundle/blob/master/CONTRIBUTING.md)
24 | file.
25 |
26 |
27 | Credits
28 | -------
29 |
30 | * William Durand
31 | * [All contributors](https://github.com/willdurand/BazingaRestExtraBundle/contributors)
32 |
33 |
34 | License
35 | -------
36 |
37 | This bundle is released under the MIT license. See the complete license in the
38 | bundle:
39 |
40 | Resources/meta/LICENSE
41 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "willdurand/rest-extra-bundle",
3 | "description": "This bundle provides extra features for your REST APIs built using Symfony2.",
4 | "keywords": [ "rest", "api" ],
5 | "type": "symfony-bundle",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "William Durand",
10 | "email": "will+git@drnd.me"
11 | }
12 | ],
13 | "require": {
14 | "symfony/framework-bundle": "^3.4|^4.0"
15 | },
16 | "require-dev": {
17 | "sensio/framework-extra-bundle": "^4.0|^5.0",
18 | "symfony/browser-kit": "^3.4|^4.0",
19 | "symfony/finder": "^3.4|^4.0",
20 | "symfony/http-foundation": "^3.4|^4.0",
21 | "symfony/phpunit-bridge": "^4.0",
22 | "symfony/yaml": "^3.4|^4.0"
23 | },
24 | "autoload": {
25 | "psr-4": { "Bazinga\\Bundle\\RestExtraBundle\\": "" }
26 | },
27 | "extra": {
28 | "branch-alias": {
29 | "dev-master": "2.1-dev"
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Resources/config/csrf_double_submit_listener.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | Bazinga\Bundle\RestExtraBundle\EventListener\CsrfDoubleSubmitListener
8 |
9 |
10 |
11 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Resources/meta/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 William Durand
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing
2 | ============
3 |
4 | First of all, **thank you** for contributing, **you are awesome**!
5 |
6 | Here are a few rules to follow in order to ease code reviews, and discussions before
7 | maintainers accept and merge your work.
8 |
9 | You MUST follow the [PSR-1](http://www.php-fig.org/psr/1/) and
10 | [PSR-2](http://www.php-fig.org/psr/2/). If you don't know about any of them, you
11 | should really read the recommendations. Can't wait? Use the [PHP-CS-Fixer
12 | tool](http://cs.sensiolabs.org/).
13 |
14 | You MUST run the test suite.
15 |
16 | You MUST write (or update) unit tests.
17 |
18 | You SHOULD write documentation.
19 |
20 | Please, write [commit messages that make
21 | sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
22 | and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing)
23 | before submitting your Pull Request.
24 |
25 | One may ask you to [squash your
26 | commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
27 | too. This is used to "clean" your Pull Request before merging it (we don't want
28 | commits such as `fix tests`, `fix 2`, `fix 3`, etc.).
29 |
30 | Thank you!
31 |
--------------------------------------------------------------------------------
/Tests/WebTestCase.php:
--------------------------------------------------------------------------------
1 | deleteTmpDir();
16 | }
17 |
18 | protected function deleteTmpDir()
19 | {
20 | if (!file_exists($dir = sys_get_temp_dir() . '/' . Kernel::VERSION)) {
21 | return;
22 | }
23 |
24 | $fs = new Filesystem();
25 | $fs->remove($dir);
26 | }
27 |
28 | protected static function getKernelClass()
29 | {
30 | require_once __DIR__ . '/Fixtures/app/AppKernel.php';
31 |
32 | return 'Bazinga\Bundle\RestExtraBundle\Tests\Functional\AppKernel';
33 | }
34 |
35 | protected static function createKernel(array $options = array())
36 | {
37 | $class = self::getKernelClass();
38 |
39 | return new $class(
40 | 'default',
41 | isset($options['debug']) ? $options['debug'] : true
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ./Tests
23 |
24 |
25 |
26 |
27 |
28 | .
29 |
30 | Resources
31 | Tests
32 | vendor
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/Test/WebTestCase.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | abstract class WebTestCase extends BaseWebTestCase
19 | {
20 | protected function assertJsonResponse($response, $statusCode = 200)
21 | {
22 | $this->assertEquals(
23 | $statusCode, $response->getStatusCode(),
24 | $response->getContent()
25 | );
26 | $this->assertTrue(
27 | $response->headers->contains('Content-Type', 'application/json'),
28 | $response->headers
29 | );
30 | }
31 |
32 | protected function jsonRequest($verb, $endpoint, array $data = array())
33 | {
34 | $data = empty($data) ? null : json_encode($data);
35 |
36 | return $this->client->request($verb, $endpoint,
37 | array(),
38 | array(),
39 | array(
40 | 'HTTP_ACCEPT' => 'application/json',
41 | 'CONTENT_TYPE' => 'application/json'
42 | ),
43 | $data
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | new Test($id));
16 | }
17 |
18 | public function getNoConventionAction($id)
19 | {
20 | return new Test($id);
21 | }
22 |
23 | /**
24 | * @ParamConverter("date", options={"format": "Y-m-d"})
25 | */
26 | public function getParamConverterAction(\DateTime $date)
27 | {
28 | return array('date' => $date);
29 | }
30 |
31 | public function linkAction($id)
32 | {
33 | return new Response();
34 | }
35 |
36 | public function allAction()
37 | {
38 | return new Response(__METHOD__);
39 | }
40 |
41 | public function allVersion123Action()
42 | {
43 | return new Response(__METHOD__);
44 | }
45 |
46 | /**
47 | * @CsrfDoubleSubmit
48 | */
49 | public function createAction()
50 | {
51 | return new Response(__METHOD__);
52 | }
53 |
54 | public function createWithoutCsrfDoubleSubmitAction()
55 | {
56 | return new Response(__METHOD__);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/DependencyInjection/BazingaRestExtraExtension.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | class BazingaRestExtraExtension extends Extension
22 | {
23 | public function load(array $configs, ContainerBuilder $container)
24 | {
25 | $config = $this->processConfiguration(new Configuration(), $configs);
26 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
27 |
28 | if (!empty($config['link_request_listener'])) {
29 | $loader->load('link_request_listener.xml');
30 | }
31 |
32 | if (!empty($config['csrf_double_submit_listener']) && true === $config['csrf_double_submit_listener']['enabled']) {
33 | $loader->load('csrf_double_submit_listener.xml');
34 |
35 | $container->getDefinition('bazinga_rest_extra.event_listener.csrf_double_submit')
36 | ->replaceArgument(1, $config['csrf_double_submit_listener']['cookie_name'])
37 | ->replaceArgument(2, $config['csrf_double_submit_listener']['parameter_name'])
38 | ;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | sudo: false
3 | cache:
4 | directories:
5 | - $HOME/.composer/cache/files
6 | - $HOME/symfony-bridge/.phpunit
7 |
8 | env:
9 | global:
10 | - PHPUNIT_FLAGS="-v"
11 | - SYMFONY_PHPUNIT_DIR="$HOME/symfony-bridge/.phpunit"
12 | - SYMFONY_PHPUNIT_VERSION="5.7.27"
13 | - SYMFONY_DEPRECATIONS_HELPER="weak"
14 |
15 | matrix:
16 | fast_finish: true
17 | include:
18 | # Minimum supported dependencies with the latest and oldest PHP version
19 | - php: 7.2
20 | env: SYMFONY_VERSION="^3.4" SYMFONY_PHPUNIT_VERSION="6.5.13"
21 | - php: 7.1
22 | env: SYMFONY_VERSION="^3.4"
23 | - php: 7.0
24 | env: SYMFONY_VERSION="^3.4"
25 | - php: 5.6
26 | env: SYMFONY_VERSION="^3.4"
27 | - php: 7.0
28 | env: SYMFONY_VERSION="^3.4" COMPOSER_FLAGS="--prefer-lowest"
29 |
30 | # Test the latest stable release
31 | - php: 7.2
32 | env: SYMFONY_VERSION="^4.0" COVERAGE=true PHPUNIT_FLAGS="-v --coverage-text" SYMFONY_PHPUNIT_VERSION="6.5.13"
33 | - php: 7.1
34 | env: SYMFONY_VERSION="^4.0"
35 |
36 | before_install:
37 | - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi
38 |
39 | install:
40 | - composer require symfony/framework-bundle:${SYMFONY_VERSION} --no-update
41 | - composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction
42 |
43 | script:
44 | - composer validate --strict --no-check-lock
45 | # simple-phpunit is the PHPUnit wrapper provided by the PHPUnit Bridge component and
46 | # it helps with testing legacy code and deprecations (composer require symfony/phpunit-bridge)
47 | - ./vendor/bin/simple-phpunit $PHPUNIT_FLAGS
48 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | class Configuration implements ConfigurationInterface
20 | {
21 | /**
22 | * Generates the configuration tree builder.
23 | *
24 | * @return TreeBuilder The tree builder
25 | */
26 | public function getConfigTreeBuilder()
27 | {
28 | $treeBuilder = new TreeBuilder();
29 | $rootNode = $treeBuilder->root('bazinga_rest_extra');
30 |
31 | $rootNode
32 | ->children()
33 | ->scalarNode('link_request_listener')->defaultFalse()->end()
34 | ->arrayNode('csrf_double_submit_listener')
35 | ->treatFalseLike(array('enabled' => false))
36 | ->treatTrueLike(array('enabled' => true))
37 | ->treatNullLike(array('enabled' => true))
38 | ->children()
39 | ->booleanNode('enabled')
40 | ->defaultFalse()
41 | ->end()
42 | ->scalarNode('cookie_name')
43 | ->cannotBeEmpty()
44 | ->end()
45 | ->scalarNode('parameter_name')
46 | ->cannotBeEmpty()
47 | ->end()
48 | ->end()
49 | ->end()
50 | ->end()
51 | ;
52 |
53 | return $treeBuilder;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Tests/Fixtures/app/AppKernel.php:
--------------------------------------------------------------------------------
1 | environment;
38 | }
39 |
40 | public function getLogDir()
41 | {
42 | return sys_get_temp_dir() . '/' . Kernel::VERSION . '/bazinga-extra-rest/logs';
43 | }
44 |
45 | public function registerContainerConfiguration(LoaderInterface $loader)
46 | {
47 | $loader->load(__DIR__ . '/config/symfony-'.self::getRoutingVersion(). '/' . $this->environment . '.yml');
48 | }
49 |
50 | private static function getRoutingVersion()
51 | {
52 | $installedPackages = json_decode(file_get_contents(__DIR__.'/../../../vendor/composer/installed.json'));
53 | foreach ($installedPackages as $package) {
54 | if($package->name === 'symfony/routing')
55 |
56 | return (int) ($package->version_normalized);
57 | }
58 |
59 | return 2;
60 | }
61 |
62 | public function serialize()
63 | {
64 | return serialize(array($this->getEnvironment(), $this->isDebug()));
65 | }
66 |
67 | public function unserialize($str)
68 | {
69 | call_user_func_array(array($this, '__construct'), unserialize($str));
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Tests/Fixtures/app/config/symfony-3/routing.yml:
--------------------------------------------------------------------------------
1 | test_get:
2 | path: /tests/{id}
3 | defaults: { _controller: BazingaRestExtraTestBundle:Test:get, _format: ~ }
4 | methods: GET
5 |
6 | test_get_no_convention:
7 | path: /tests/noconventions/{id}
8 | defaults: { _controller: BazingaRestExtraTestBundle:Test:getNoConvention, _format: ~ }
9 | methods: GET
10 |
11 | test_get_param_converter:
12 | path: /tests/paramconverter/{date}
13 | defaults: { _controller: BazingaRestExtraTestBundle:Test:getParamConverter, _format: ~ }
14 | methods: GET
15 |
16 | test_link:
17 | path: /tests/{id}
18 | defaults: { _controller: BazingaRestExtraTestBundle:Test:link, _format: ~ }
19 | methods: LINK
20 |
21 | test_all:
22 | path: /tests
23 | defaults: { _controller: BazingaRestExtraTestBundle:Test:all, _format: ~ }
24 | methods: GET
25 | requirements:
26 | _api_version: "1"
27 |
28 | test_all_version_123:
29 | path: /tests
30 | defaults: { _controller: BazingaRestExtraTestBundle:Test:allVersion123, _format: ~ }
31 | methods: GET
32 | requirements:
33 | _api_version: "123"
34 |
35 | test_create:
36 | path: /tests
37 | defaults: { _controller: BazingaRestExtraTestBundle:Test:create, _format: ~ }
38 | methods: POST
39 |
40 | test_create_without_csrf_double_submit:
41 | path: /without-csrf-double-submit
42 | defaults: { _controller: BazingaRestExtraTestBundle:Test:createWithoutCsrfDoubleSubmit, _format: ~ }
43 | methods: POST
44 |
45 | test_create_csrf_class:
46 | path: /tests-csrf-class
47 | defaults: { _controller: BazingaRestExtraTestBundle:TestCsrf:create, _format: ~ }
48 | methods: POST
49 |
50 | test_get_csrf_class:
51 | path: /tests-csrf-class
52 | defaults: { _controller: BazingaRestExtraTestBundle:TestCsrf:get, _format: ~ }
53 | methods: GET
54 |
55 | test_invoke_csrf_class:
56 | path: /tests/invoke
57 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestInvokeCsrfController, _format: ~ }
58 | methods: POST
59 |
60 | test_invoke:
61 | path: /tests/invoke-without-csrf
62 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestInvokeController, _format: ~ }
63 | methods: POST
64 |
--------------------------------------------------------------------------------
/Tests/Fixtures/app/config/symfony-4/routing.yml:
--------------------------------------------------------------------------------
1 | test_get:
2 | path: /tests/{id}
3 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestController::getAction, _format: ~ }
4 | methods: GET
5 |
6 | test_get_no_convention:
7 | path: /tests/noconventions/{id}
8 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestController::getNoConventionAction, _format: ~ }
9 | methods: GET
10 |
11 | test_get_param_converter:
12 | path: /tests/paramconverter/{date}
13 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestController::getParamConverterAction, _format: ~ }
14 | methods: GET
15 |
16 | test_link:
17 | path: /tests/{id}
18 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestController::linkAction, _format: ~ }
19 | methods: LINK
20 |
21 | test_all:
22 | path: /tests
23 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestController::allAction, _format: ~ }
24 | methods: GET
25 | requirements:
26 | _api_version: "1"
27 |
28 | test_all_version_123:
29 | path: /tests
30 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestController::allVersion123Action, _format: ~ }
31 | methods: GET
32 | requirements:
33 | _api_version: "123"
34 |
35 | test_create:
36 | path: /tests
37 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestController::createAction, _format: ~ }
38 | methods: POST
39 |
40 | test_create_without_csrf_double_submit:
41 | path: /without-csrf-double-submit
42 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestController::createWithoutCsrfDoubleSubmitAction, _format: ~ }
43 | methods: POST
44 |
45 | test_create_csrf_class:
46 | path: /tests-csrf-class
47 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestCsrfController::createAction, _format: ~ }
48 | methods: POST
49 |
50 | test_get_csrf_class:
51 | path: /tests-csrf-class
52 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestCsrfController::getAction, _format: ~ }
53 | methods: GET
54 |
55 | test_invoke_csrf_class:
56 | path: /tests/invoke
57 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestInvokeCsrfController, _format: ~ }
58 | methods: POST
59 |
60 | test_invoke:
61 | path: /tests/invoke-without-csrf
62 | defaults: { _controller: Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestInvokeController, _format: ~ }
63 | methods: POST
64 |
--------------------------------------------------------------------------------
/Tests/EventListener/LinkRequestListenerTest.php:
--------------------------------------------------------------------------------
1 | createClient();
15 | $headers = array(
16 | 'HTTP_LINK' => sprintf($linkUri, 2),
17 | 'HTTP_ORIGIN' => 'http://localhost',
18 | );
19 |
20 | if (true === $forceScriptName) {
21 | $headers['SCRIPT_FILENAME'] = '/app.php';
22 | $headers['SCRIPT_NAME'] = '/app.php';
23 | $uri = '/app.php' . $uri;
24 | }
25 |
26 | $client->request('LINK', $uri,
27 | array(),
28 | array(),
29 | $headers
30 | );
31 | $request = $client->getRequest();
32 |
33 | $this->assertTrue($request->attributes->has('links'));
34 |
35 | $links = $request->attributes->get('links');
36 | $this->assertCount(1, $links);
37 |
38 | $link = array_shift($links);
39 | $object = is_array($link) ? $link[0] : $link;
40 |
41 | $this->assertInstanceOf('Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Model\Test', $object);
42 | $this->assertEquals(2, $object->getId());
43 | }
44 |
45 | public function testLinkSupportsParamConverter()
46 | {
47 | $client = $this->createClient();
48 | $client->request('LINK', '/tests/1',
49 | array(),
50 | array(),
51 | array('HTTP_LINK' => '', 'HTTP_ORIGIN' => 'http://localhost')
52 | );
53 | $request = $client->getRequest();
54 |
55 | $this->assertTrue($request->attributes->has('links'));
56 |
57 | $links = $request->attributes->get('links');
58 | $this->assertCount(1, $links);
59 |
60 | $link = array_shift($links);
61 | $object = is_array($link) ? $link[0] : $link;
62 |
63 | $this->assertInstanceOf('DateTime', $object);
64 | $this->assertEquals('2013-12-10', $object->format('Y-m-d'));
65 | }
66 |
67 | public static function dataProviderForTestLink()
68 | {
69 | return array(
70 | array('/tests/1', ''),
71 | array('/tests/1', ''),
72 | array('/tests/1', '', true),
73 | array('/tests/1', ''),
74 | array('/tests/1', '; rel="test"')
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/EventListener/CsrfDoubleSubmitListener.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | class CsrfDoubleSubmitListener
21 | {
22 | /**
23 | * @var Reader
24 | */
25 | private $annotationReader;
26 |
27 | /**
28 | * @var string
29 | */
30 | private $cookieName;
31 |
32 | /**
33 | * @var string
34 | */
35 | private $parameterName;
36 |
37 | /**
38 | * @param Reader $annotationReader
39 | * @param string $cookieName
40 | * @param string $parameterName
41 | */
42 | public function __construct(Reader $annotationReader, $cookieName, $parameterName)
43 | {
44 | $this->annotationReader = $annotationReader;
45 | $this->cookieName = $cookieName;
46 | $this->parameterName = $parameterName;
47 | }
48 |
49 | public function onKernelController(FilterControllerEvent $event)
50 | {
51 | $controller = $event->getController();
52 | $request = $event->getRequest();
53 |
54 | // these HTTP methods do not require CSRF protection
55 | if (in_array($request->getMethod(), array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) {
56 | return;
57 | }
58 |
59 | if (is_array($controller)) {
60 | $object = new \ReflectionObject($controller[0]);
61 | $method = $object->getMethod($controller[1]);
62 | } elseif (is_object($controller) && method_exists($controller, '__invoke')) {
63 | $object = new \ReflectionObject($controller);
64 | $method = $object->getMethod('__invoke');
65 | } else {
66 | return;
67 | }
68 |
69 | if (false === $this->isProtectedByCsrfDoubleSubmit($object, $method)) {
70 | return;
71 | }
72 |
73 | $cookieValue = $request->cookies->get($this->cookieName);
74 | $paramValue = $request->request->get($this->parameterName);
75 |
76 | if (empty($cookieValue)) {
77 | throw new HttpException(400, 'Cookie not found or invalid.');
78 | }
79 |
80 | if (empty($paramValue)) {
81 | throw new HttpException(400, 'Request parameter not found or invalid.');
82 | }
83 |
84 | if (0 !== strcmp($cookieValue, $paramValue)) {
85 | throw new HttpException(400, 'CSRF values mismatch.');
86 | }
87 |
88 | $request->cookies->remove($this->cookieName);
89 | $request->request->remove($this->parameterName);
90 | }
91 |
92 | /**
93 | * @return boolean
94 | */
95 | private function isProtectedByCsrfDoubleSubmit(\ReflectionClass $class, \ReflectionMethod $method)
96 | {
97 | $annotation = $this->annotationReader->getClassAnnotation(
98 | $class,
99 | 'Bazinga\Bundle\RestExtraBundle\Annotation\CsrfDoubleSubmit'
100 | );
101 |
102 | if (null !== $annotation) {
103 | return true;
104 | }
105 |
106 | $annotation = $this->annotationReader->getMethodAnnotation(
107 | $method,
108 | 'Bazinga\Bundle\RestExtraBundle\Annotation\CsrfDoubleSubmit'
109 | );
110 |
111 | return null !== $annotation;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Resources/doc/index.md:
--------------------------------------------------------------------------------
1 | BazingaRestExtraBundle
2 | ======================
3 |
4 | This bundle provides extra features for your REST APIs built using Symfony2.
5 |
6 | Installation
7 | ------------
8 |
9 | $ composer.phar require willdurand/rest-extra-bundle
10 |
11 |
12 | Register the bundle in `app/AppKernel.php`:
13 |
14 | ``` php
15 | // app/AppKernel.php
16 | public function registerBundles()
17 | {
18 | return array(
19 | // ...
20 | new Bazinga\Bundle\RestExtraBundle\BazingaRestExtraBundle(),
21 | );
22 | }
23 | ```
24 |
25 | Enable the bundle's configuration in `app/config/config.yml`:
26 |
27 | ``` yaml
28 | # app/config/config.yml
29 | bazinga_rest_extra: ~
30 | ```
31 |
32 |
33 | Usage
34 | -----
35 |
36 | In the following, you will find the documentation for all features provided by
37 | this bundle.
38 |
39 | ### Listeners
40 |
41 | #### CsrfDoubleSubmitListener
42 |
43 | The `CsrfDoubleSubmitListener` listener is a way to protect you against CSRF
44 | attacks by leveraging the client side instead of the plain old server side. It
45 | is particularly useful for REST APIs as the Symfony2 CSRF protection relies on
46 | the session in order to store the secret. That is why you often have to disable
47 | CSRF protection when you use Forms in your REST APIs.
48 |
49 |
50 |
51 | Using the **double submit** mechanism, there is no need to store anything on the
52 | server. However, the client MUST:
53 |
54 | * generate a random secret;
55 | * set a cookie with this secret;
56 | * send this secret as part of the request parameters.
57 |
58 | For further information, you can read more about [Stateless CSRF
59 | protection](http://appsandsecurity.blogspot.se/2012/01/stateless-csrf-protection.html)
60 | and [Stateful vs Stateless CSRF
61 | Defences](http://blog.astrumfutura.com/2013/08/stateful-vs-stateless-csrf-defences-know-the-difference/).
62 |
63 | Here is the configuration section for this listener. First, you must enable it,
64 | then you have to choose names for both `cookie_name` and `parameter_name`
65 | configuration parameters:
66 |
67 | ``` yaml
68 | # app/config/config.yml
69 | bazinga_rest_extra:
70 | csrf_double_submit_listener:
71 | enabled: true
72 | cookie_name: cookie_csrf
73 | parameter_name: _csrf_token
74 | ```
75 |
76 | Once done, you can configure each action you want to protect with **CSRF double
77 | submit** mechanism by using the `@CsrfDoubleSubmit` annotation:
78 |
79 | ``` php
80 | use Bazinga\Bundle\RestExtraBundle\Annotation\CsrfDoubleSubmit;
81 |
82 | // ...
83 |
84 | /**
85 | * @CsrfDoubleSubmit
86 | */
87 | public function createAction()
88 | {
89 | // ...
90 | }
91 | ```
92 |
93 | Or you could protect a controller with the **CSRF double submit** mechanism
94 | by using the `@CsrfDoubleSubmit` annotation, all methods except `GET, HEAD, OPTIONS, TRACE`
95 | will be protected:
96 |
97 | ``` php
98 | use Bazinga\Bundle\RestExtraBundle\Annotation\CsrfDoubleSubmit;
99 |
100 | // ...
101 |
102 | /**
103 | * @CsrfDoubleSubmit
104 | */
105 | class ApiController
106 | {
107 | // ...
108 | }
109 | ```
110 |
111 | #### LinkRequestListener
112 |
113 | The `LinkRequestListener` listener is able to convert **links**, as described in
114 | [RFC 5988](http://tools.ietf.org/html/rfc5988) and covered in [this blog post
115 | about REST APIs with Symfony2](http://williamdurand.fr/2012/08/02/rest-apis-with-symfony2-the-right-way/#the-friendship-algorithm),
116 | into PHP **objects**. This listener makes two **strong** assumptions:
117 |
118 | * Your `getAction()` action (naming does not matter here), also known as the
119 | action used to retrieve a specific resource must take the `identifier` as is;
120 |
121 | * This method MUST return an `array`, such as `array('user' => $user)` **OR** return the object itself, such as `$user`.
122 |
123 | If it is ok for you, then turn the listener on in the configuration:
124 |
125 | ``` yaml
126 | # app/config/config.yml
127 | bazinga_rest_extra:
128 | link_request_listener: true
129 | ```
130 |
131 | Now you can retrieve your objects in the Request's attributes:
132 |
133 | ``` php
134 | if (!$request->attributes->has('links')) {
135 | throw new HttpException(400, 'No links found');
136 | }
137 |
138 | // When *not* using rel in the links (e.g. Link: )
139 | foreach ($request->attributes->get('links') as $linkObject) {
140 | // ...
141 | }
142 |
143 | // When using rel in the links (e.g. Link: ; rel="context1", ; rel="context2")
144 | foreach ($request->attributes->get('links') as $context => $links) {
145 | foreach ($links as $linkObject) {
146 | // ...
147 | }
148 | }
149 | ```
150 |
151 | ### Testing
152 |
153 | The bundle provides a `WebTestCase` class that provides useful methods for
154 | testing your REST APIs.
155 |
156 | * The `assertJsonResponse()` method allows you to assert that you got a JSON
157 | response:
158 |
159 | ``` php
160 | $client = static::createClient();
161 | $crawler = $client->request('GET', '/users');
162 | $response = $client->getResponse();
163 |
164 | $this->assertJsonResponse($response);
165 | ```
166 |
167 | * The `jsonRequest()` method allows you to perform a JSON request by setting both
168 | the `Content-Type` and the `Accept` headers to `application/json`:
169 |
170 | ``` php
171 | $client = static::createClient();
172 | $crawler = $this->jsonRequest('GET', '/users/123', $data = array());
173 | $response = $client->getResponse();
174 | ```
175 |
176 | Reference Configuration
177 | -----------------------
178 |
179 | ``` yaml
180 | bazinga_rest_extra:
181 | link_request_listener: false
182 | csrf_double_submit_listener:
183 | enabled: false
184 | cookie_name: ~
185 | parameter_name: ~
186 | ```
187 |
188 |
189 | Testing
190 | -------
191 |
192 | Setup the test suite using [Composer](http://getcomposer.org/):
193 |
194 | $ composer install --dev
195 |
196 | Run it using PHPUnit:
197 |
198 | $ phpunit
199 |
--------------------------------------------------------------------------------
/EventListener/LinkRequestListener.php:
--------------------------------------------------------------------------------
1 |
28 | * @author Samuel Gordalina
29 | */
30 | class LinkRequestListener
31 | {
32 | /**
33 | * @var ControllerResolverInterface
34 | */
35 | private $resolver;
36 |
37 | /**
38 | * @var UrlMatcherInterface
39 | */
40 | private $urlMatcher;
41 |
42 | /**
43 | * @var ArgumentResolverInterface
44 | */
45 | private $argumentResolver;
46 |
47 | /**
48 | * @param ControllerResolverInterface $controllerResolver The 'controller_resolver' service
49 | * @param UrlMatcherInterface $urlMatcher The 'router' service
50 | */
51 | public function __construct(ControllerResolverInterface $controllerResolver, UrlMatcherInterface $urlMatcher, ArgumentResolverInterface $argumentResolver = null)
52 | {
53 | $this->resolver = $controllerResolver;
54 | $this->urlMatcher = $urlMatcher;
55 |
56 | if (null === $this->argumentResolver) {
57 | $this->argumentResolver = new ArgumentResolver();
58 | }
59 | }
60 |
61 | public function onKernelRequest(GetResponseEvent $event, $eventName = null, EventDispatcherInterface $eventDispatcher = null)
62 | {
63 | if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
64 | return;
65 | }
66 |
67 | if (!$event->getRequest()->headers->has('link')) {
68 | return;
69 | }
70 |
71 | if (!$eventDispatcher) {
72 | // BC for symfony < 2.4: $eventDispatcher is not passed to the function as a parameter.
73 | $eventDispatcher = $event->getDispatcher();
74 | }
75 |
76 | $links = array();
77 | $header = $event->getRequest()->headers->get('link');
78 |
79 | /*
80 | * Due to limitations, multiple same-name headers are sent as comma
81 | * separated values.
82 | *
83 | * This breaks those headers into Link headers following the format
84 | * http://tools.ietf.org/html/rfc2068#section-19.6.2.4
85 | */
86 | while (preg_match('/^((?:[^"]|"[^"]*")*?),/', $header, $matches)) {
87 | $header = trim(substr($header, strlen($matches[0])));
88 | $links[] = $matches[1];
89 | }
90 |
91 | if ($header) {
92 | $links[] = $header;
93 | }
94 |
95 | $requestMethod = $this->urlMatcher->getContext()->getMethod();
96 | // Force the GET method to avoid the use of the
97 | // previous method (LINK/UNLINK)
98 | $this->urlMatcher->getContext()->setMethod('GET');
99 |
100 | // The controller resolver needs a request to resolve the controller.
101 | $stubRequest = new Request();
102 |
103 | foreach ($links as $idx => $link) {
104 | $linkHeader = $this->parseLinkHeader($link);
105 | $resource = $this->parseResource($linkHeader, $event->getRequest());
106 |
107 | try {
108 | $route = $this->urlMatcher->match($resource);
109 | } catch (\Exception $e) {
110 | // If we don't have a matching route we return
111 | // the original Link header
112 | continue;
113 | }
114 |
115 | $stubRequest->attributes->replace($route);
116 |
117 | if (false === $controller = $this->resolver->getController($stubRequest)) {
118 | continue;
119 | }
120 |
121 | // Make sure @ParamConverter and some other annotations are called
122 | $subEvent = new FilterControllerEvent($event->getKernel(), $controller, $stubRequest, HttpKernelInterface::SUB_REQUEST);
123 | $eventDispatcher->dispatch(KernelEvents::CONTROLLER, $subEvent);
124 | $controller = $subEvent->getController();
125 |
126 | $arguments = $this->argumentResolver->getArguments($stubRequest, $controller);
127 |
128 | $subEvent = new FilterControllerArgumentsEvent($event->getKernel(), $controller, $arguments, $stubRequest, HttpKernelInterface::SUB_REQUEST);
129 | $eventDispatcher->dispatch(KernelEvents::CONTROLLER_ARGUMENTS, $subEvent);
130 | $controller = $subEvent->getController();
131 | $arguments = $subEvent->getArguments();
132 |
133 | try {
134 | $result = call_user_func_array($controller, $arguments);
135 |
136 | $value = is_array($result) ? current($result) : $result;
137 |
138 | if ($linkHeader->hasRel()) {
139 | unset($links[$idx]);
140 | $links[$linkHeader->getRel()][] = $value;
141 | } else {
142 | $links[$idx] = $value;
143 | }
144 |
145 | } catch (\Exception $e) {
146 | continue;
147 | }
148 | }
149 |
150 | $event->getRequest()->attributes->set('links', $links);
151 | $this->urlMatcher->getContext()->setMethod($requestMethod);
152 | }
153 |
154 | /**
155 | * @param string $link
156 | *
157 | * @return LinkHeader
158 | */
159 | protected function parseLinkHeader($link)
160 | {
161 | $linkParams = explode(';', trim($link));
162 |
163 | $url = array_shift($linkParams);
164 | $url = preg_replace('/<|>/', '', $url);
165 |
166 | $rel = empty($linkParams) ? '' : preg_replace("/rel=\"(.*)\"/", "$1", trim($linkParams[0]));
167 |
168 | return new LinkHeader($url, $rel);
169 | }
170 |
171 | /**
172 | * @param LinkHeader $linkHeader
173 | * @param Request $request
174 | *
175 | * @return string
176 | */
177 | private function parseResource($linkHeader, $request)
178 | {
179 | // Link needs to be cleaned from 'http://host/basepath' when added
180 | $httpSchemaAndBasePath = $request->getSchemeAndHttpHost() . $request->getBaseUrl();
181 |
182 | return str_replace($httpSchemaAndBasePath, '', $linkHeader->getUrl());
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/Tests/EventListener/CsrfDoubleSubmitListenerTest.php:
--------------------------------------------------------------------------------
1 | createClient();
15 | $client->getCookieJar()->set(new Cookie('csrf_cookie', $csrfValue));
16 |
17 | $crawler = $client->request('POST', '/tests', array(
18 | '_csrf_token' => $csrfValue,
19 | ));
20 |
21 | $request = $client->getRequest();
22 | $response = $client->getResponse();
23 |
24 | $this->assertEquals(200, $response->getStatusCode());
25 | $this->assertEquals(
26 | 'Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestController::createAction',
27 | $response->getContent()
28 | );
29 | $this->assertCount(0, $response->headers->getCookies());
30 | }
31 |
32 | /**
33 | * @expectedException Symfony\Component\HttpKernel\Exception\HttpException
34 | * @expectedExceptionMessage Cookie not found or invalid.
35 | */
36 | public function testCsrfDoubleSubmitFailsIfNoCookieFound()
37 | {
38 | $client = $this->createClient();
39 | $crawler = $client->request('POST', '/tests', array(
40 | '_csrf_token' => '',
41 | ));
42 | }
43 |
44 | /**
45 | * @expectedException Symfony\Component\HttpKernel\Exception\HttpException
46 | * @expectedExceptionMessage Cookie not found or invalid.
47 | *
48 | * @dataProvider dataProviderWithInvalidData
49 | */
50 | public function testCsrfDoubleSubmitFailsIfInvalidCookieValue($cookieValue)
51 | {
52 | $client = $this->createClient();
53 | $client->getCookieJar()->set(new Cookie('csrf_cookie', $cookieValue));
54 |
55 | $crawler = $client->request('POST', '/tests');
56 | }
57 |
58 | /**
59 | * @expectedException Symfony\Component\HttpKernel\Exception\HttpException
60 | * @expectedExceptionMessage Request parameter not found or invalid.
61 | */
62 | public function testCsrfDoubleSubmitFailsIfNoRequestParameterFound()
63 | {
64 | $client = $this->createClient();
65 | $client->getCookieJar()->set(new Cookie('csrf_cookie', 'a token'));
66 |
67 | $crawler = $client->request('POST', '/tests');
68 | }
69 |
70 | /**
71 | * @expectedException Symfony\Component\HttpKernel\Exception\HttpException
72 | * @expectedExceptionMessage Request parameter not found or invalid.
73 | *
74 | * @dataProvider dataProviderWithInvalidData
75 | */
76 | public function testCsrfDoubleSubmitFailsIfInvalidRequestParamValue($paramValue)
77 | {
78 | $client = $this->createClient();
79 | $client->getCookieJar()->set(new Cookie('csrf_cookie', 'a token'));
80 |
81 | $crawler = $client->request('POST', '/tests', array(
82 | '_csrf_token' => $paramValue,
83 | ));
84 | }
85 |
86 | /**
87 | * @expectedException Symfony\Component\HttpKernel\Exception\HttpException
88 | * @expectedExceptionMessage CSRF values mismatch.
89 | */
90 | public function testCsrfDoubleSubmitFailsIfValuesMismatch()
91 | {
92 | $client = $this->createClient();
93 | $client->getCookieJar()->set(new Cookie('csrf_cookie', 'a token'));
94 |
95 | $crawler = $client->request('POST', '/tests', array(
96 | '_csrf_token' => 'another token',
97 | ));
98 | }
99 |
100 | public function testCsrfDoubleSubmitWithoutAnnotationIsInactive()
101 | {
102 | $client = $this->createClient();
103 | $crawler = $client->request('POST', '/without-csrf-double-submit');
104 | $response = $client->getResponse();
105 |
106 | $this->assertEquals(200, $response->getStatusCode());
107 | $this->assertEquals(
108 | 'Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestController::createWithoutCsrfDoubleSubmitAction',
109 | $response->getContent()
110 | );
111 | }
112 |
113 | public function testCsrfDoubleSubmitClass()
114 | {
115 | $csrfValue = 'Sup3r$ecr3t';
116 |
117 | $client = $this->createClient();
118 | $client->getCookieJar()->set(new Cookie('csrf_cookie', $csrfValue));
119 |
120 | $crawler = $client->request('POST', '/tests-csrf-class', array(
121 | '_csrf_token' => $csrfValue,
122 | ));
123 |
124 | $request = $client->getRequest();
125 | $response = $client->getResponse();
126 |
127 | $this->assertEquals(200, $response->getStatusCode());
128 | $this->assertEquals(
129 | 'Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestCsrfController::createAction',
130 | $response->getContent()
131 | );
132 | $this->assertCount(0, $response->headers->getCookies());
133 | }
134 |
135 | /**
136 | * @expectedException Symfony\Component\HttpKernel\Exception\HttpException
137 | * @expectedExceptionMessage Cookie not found or invalid.
138 | */
139 | public function testCsrfDoubleSubmitClassFailsIfNoCookieFound()
140 | {
141 | $client = $this->createClient();
142 | $crawler = $client->request('POST', '/tests-csrf-class', array(
143 | '_csrf_token' => '',
144 | ));
145 | }
146 |
147 | public function testCsrfDoubleSubmitClassGETMethod()
148 | {
149 | $client = $this->createClient();
150 |
151 | $crawler = $client->request('GET', '/tests-csrf-class');
152 |
153 | $request = $client->getRequest();
154 | $response = $client->getResponse();
155 |
156 | $this->assertEquals(200, $response->getStatusCode());
157 | $this->assertEquals(
158 | 'Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestCsrfController::getAction',
159 | $response->getContent()
160 | );
161 | }
162 |
163 | public function testInvokeCsrfDoubleSubmit()
164 | {
165 | $csrfValue = 'Sup3r$ecr3t';
166 |
167 | $client = $this->createClient();
168 | $client->getCookieJar()->set(new Cookie('csrf_cookie', $csrfValue));
169 |
170 | $crawler = $client->request('POST', '/tests/invoke', array(
171 | '_csrf_token' => $csrfValue,
172 | ));
173 |
174 | $request = $client->getRequest();
175 | $response = $client->getResponse();
176 |
177 | $this->assertEquals(200, $response->getStatusCode());
178 | $this->assertEquals(
179 | 'Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestInvokeCsrfController::__invoke',
180 | $response->getContent()
181 | );
182 | $this->assertCount(0, $response->headers->getCookies());
183 | }
184 |
185 | /**
186 | * @expectedException Symfony\Component\HttpKernel\Exception\HttpException
187 | * @expectedExceptionMessage Cookie not found or invalid.
188 | */
189 | public function testInvokeCsrfDoubleSubmitFailsIfNoCookieFound()
190 | {
191 | $client = $this->createClient();
192 | $crawler = $client->request('POST', '/tests/invoke', array(
193 | '_csrf_token' => '',
194 | ));
195 | }
196 |
197 | /**
198 | * @expectedException Symfony\Component\HttpKernel\Exception\HttpException
199 | * @expectedExceptionMessage Cookie not found or invalid.
200 | *
201 | * @dataProvider dataProviderWithInvalidData
202 | */
203 | public function testInvokeCsrfDoubleSubmitFailsIfInvalidCookieValue($cookieValue)
204 | {
205 | $client = $this->createClient();
206 | $client->getCookieJar()->set(new Cookie('csrf_cookie', $cookieValue));
207 |
208 | $crawler = $client->request('POST', '/tests/invoke');
209 | }
210 |
211 | /**
212 | * @expectedException Symfony\Component\HttpKernel\Exception\HttpException
213 | * @expectedExceptionMessage Request parameter not found or invalid.
214 | */
215 | public function testInvokeCsrfDoubleSubmitFailsIfNoRequestParameterFound()
216 | {
217 | $client = $this->createClient();
218 | $client->getCookieJar()->set(new Cookie('csrf_cookie', 'a token'));
219 |
220 | $crawler = $client->request('POST', '/tests/invoke');
221 | }
222 |
223 | /**
224 | * @expectedException Symfony\Component\HttpKernel\Exception\HttpException
225 | * @expectedExceptionMessage Request parameter not found or invalid.
226 | *
227 | * @dataProvider dataProviderWithInvalidData
228 | */
229 | public function testInvokeCsrfDoubleSubmitFailsIfInvalidRequestParamValue($paramValue)
230 | {
231 | $client = $this->createClient();
232 | $client->getCookieJar()->set(new Cookie('csrf_cookie', 'a token'));
233 |
234 | $crawler = $client->request('POST', '/tests/invoke', array(
235 | '_csrf_token' => $paramValue,
236 | ));
237 | }
238 |
239 | /**
240 | * @expectedException Symfony\Component\HttpKernel\Exception\HttpException
241 | * @expectedExceptionMessage CSRF values mismatch.
242 | */
243 | public function testInvokeCsrfDoubleSubmitFailsIfValuesMismatch()
244 | {
245 | $client = $this->createClient();
246 | $client->getCookieJar()->set(new Cookie('csrf_cookie', 'a token'));
247 |
248 | $crawler = $client->request('POST', '/tests/invoke', array(
249 | '_csrf_token' => 'another token',
250 | ));
251 | }
252 |
253 | public function testInvokeNonCsrfDoubleSubmitWithoutAnnotationIsInactive()
254 | {
255 | $client = $this->createClient();
256 | $crawler = $client->request('POST', '/tests/invoke-without-csrf');
257 | $response = $client->getResponse();
258 |
259 | $this->assertEquals(200, $response->getStatusCode());
260 | $this->assertEquals(
261 | 'Bazinga\Bundle\RestExtraBundle\Tests\Fixtures\Controller\TestInvokeController::__invoke',
262 | $response->getContent()
263 | );
264 | }
265 |
266 | public static function dataProviderWithInvalidData()
267 | {
268 | return array(
269 | array(null),
270 | array(false),
271 | array(''),
272 | );
273 | }
274 | }
275 |
--------------------------------------------------------------------------------