├── .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 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BazingaRestExtraBundle 2 | ====================== 3 | 4 | [![Build Status](https://secure.travis-ci.org/willdurand/BazingaRestExtraBundle.png)](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 | 12 | 13 | 14 | 15 | 16 | 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 | --------------------------------------------------------------------------------