diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index abdca9a2e9..41060baf97 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -312,6 +312,18 @@ class SecurityExtension extends Extension foreach ($firewall['logout']['handlers'] as $handlerId) { $listener->addMethodCall('addHandler', array(new Reference($handlerId))); } + + // register with LogoutUrlHelper + $container + ->getDefinition('templating.helper.logout_url') + ->addMethodCall('registerListener', array( + $id, + $firewall['logout']['path'], + $firewall['logout']['intention'], + $firewall['logout']['csrf_parameter'], + isset($firewall['logout']['csrf_provider']) ? new Reference($firewall['logout']['csrf_provider']) : null, + )) + ; } // Authentication listeners diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_php.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_php.xml index f2c2b121c8..83cb5597c1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_php.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_php.xml @@ -5,10 +5,17 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + Symfony\Bundle\SecurityBundle\Templating\Helper\LogoutUrlHelper Symfony\Bundle\SecurityBundle\Templating\Helper\SecurityHelper + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml index 14b0414d06..5271c1434e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml @@ -5,9 +5,16 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + Symfony\Bundle\SecurityBundle\Twig\Extension\LogoutUrlExtension Symfony\Bundle\SecurityBundle\Twig\Extension\SecurityExtension + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php new file mode 100644 index 0000000000..0d5229c928 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Templating\Helper; + +use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Templating\Helper\Helper; + +/** + * LogoutUrlHelper provides generator functions for the logout URL. + * + * @author Jeremy Mikola + */ +class LogoutUrlHelper extends Helper +{ + private $listeners; + private $request; + private $router; + + /** + * Constructor. + * + * @param Request $request A request instance + * @param UrlGeneratorInterface $router A Router instance + */ + public function __construct(Request $request, UrlGeneratorInterface $router) + { + $this->request = $request; + $this->router = $router; + $this->listeners = array(); + } + + /** + * Registers a firewall's LogoutListener, allowing its URL to be generated. + * + * @param string $key The firewall key + * @param string $logoutPath The path that starts the logout process + * @param string $intention The intention for CSRF token generation + * @param string $csrfParameter The CSRF token parameter name + * @param CsrfProviderInterface $csrfProvider A CsrfProviderInterface instance + */ + public function registerListener($key, $logoutPath, $intention, $csrfParameter, CsrfProviderInterface $csrfProvider = null) + { + $this->listeners[$key] = array($logoutPath, $intention, $csrfParameter, $csrfProvider); + } + + /** + * Generate the relative logout URL for the firewall. + * + * @param string $key The firewall key + * @return string The relative logout URL + */ + public function getLogoutPath($key) + { + return $this->generateLogoutUrl($key, false); + } + + /** + * Generate the absolute logout URL for the firewall. + * + * @param string $key The firewall key + * @return string The absolute logout URL + */ + public function getLogoutUrl($key) + { + return $this->generateLogoutUrl($key, true); + } + + /** + * Generate the logout URL for the firewall. + * + * @param string $key The firewall key + * @param Boolean $absolute Whether to generate an absolute URL + * @return string The logout URL + * @throws InvalidArgumentException if no LogoutListener is registered for the key + */ + private function generateLogoutUrl($key, $absolute) + { + if (!array_key_exists($key, $this->listeners)) { + throw new \InvalidArgumentException(sprintf('No LogoutListener found for firewall key "%s".', $key)); + } + + list($logoutPath, $intention, $csrfParameter, $csrfProvider) = $this->listeners[$key]; + + $parameters = null !== $csrfProvider ? array($csrfParameter => $csrfProvider->generateCsrfToken($intention)) : array(); + + if ('/' === $logoutPath[0]) { + $url = ($absolute ? $this->request->getUriForPath($logoutPath) : $this->request->getBasePath() . $logoutPath); + + if (!empty($parameters)) { + $url .= '?' . http_build_query($parameters); + } + } else { + $url = $this->router->generate($logoutPath, $parameters, $absolute); + } + + return $url; + } + + /** + * Returns the canonical name of this helper. + * + * @return string The canonical name + */ + public function getName() + { + return 'logout_url'; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Resources/views/Login/after_login.html.twig b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Resources/views/Login/after_login.html.twig index 9972b48ae7..b56c186ecb 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Resources/views/Login/after_login.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/CsrfFormLoginBundle/Resources/views/Login/after_login.html.twig @@ -2,5 +2,7 @@ {% block body %} Hello {{ app.user.username }}!

- You're browsing to path "{{ app.request.pathInfo }}". + You're browsing to path "{{ app.request.pathInfo }}".

+ Log out. + Log out. {% endblock %} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php index 52b533c591..1440c79425 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php @@ -19,7 +19,7 @@ class CsrfFormLoginTest extends WebTestCase /** * @dataProvider getConfigs */ - public function testFormLogin($config) + public function testFormLoginAndLogoutWithCsrfTokens($config) { $client = $this->createClient(array('test_case' => 'CsrfFormLogin', 'root_config' => $config)); $client->insulate(); @@ -31,9 +31,20 @@ class CsrfFormLoginTest extends WebTestCase $this->assertRedirect($client->getResponse(), '/profile'); - $text = $client->followRedirect()->text(); + $crawler = $client->followRedirect(); + + $text = $crawler->text(); $this->assertContains('Hello johannes!', $text); $this->assertContains('You\'re browsing to path "/profile".', $text); + + $logoutLinks = $crawler->selectLink('Log out')->links(); + $this->assertEquals(2, count($logoutLinks)); + $this->assertContains('_csrf_token=', $logoutLinks[0]->getUri()); + $this->assertSame($logoutLinks[0]->getUri(), $logoutLinks[1]->getUri()); + + $client->click($logoutLinks[0]); + + $this->assertRedirect($client->getResponse(), '/'); } /** diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/config.yml index a4d77c2c3e..1f1fa85d0a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/config.yml @@ -38,6 +38,10 @@ security: csrf_parameter: "user_login[_token]" csrf_provider: form.csrf_provider anonymous: ~ + logout: + path: /logout_path + target: / + csrf_provider: form.csrf_provider access_control: - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Twig/Extension/LogoutUrlExtension.php b/src/Symfony/Bundle/SecurityBundle/Twig/Extension/LogoutUrlExtension.php new file mode 100644 index 0000000000..bad5e79ee8 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Twig/Extension/LogoutUrlExtension.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Twig\Extension; + +use Symfony\Bundle\SecurityBundle\Templating\Helper\LogoutUrlHelper; + +/** + * LogoutUrlHelper provides generator functions for the logout URL to Twig. + * + * @author Jeremy Mikola + */ +class LogoutUrlExtension extends \Twig_Extension +{ + private $helper; + + /** + * Constructor. + * + * @param LogoutUrlHelper $helper + */ + public function __construct(LogoutUrlHelper $helper) + { + $this->helper = $helper; + } + + /** + * @see Twig_Extension::getFunctions() + */ + public function getFunctions() + { + return array( + 'logout_url' => new \Twig_Function_Method($this, 'getLogoutUrl'), + 'logout_path' => new \Twig_Function_Method($this, 'getLogoutPath'), + ); + } + + /** + * Generate the relative logout URL for the firewall. + * + * @param string $key The firewall key + * @return string The relative logout URL + */ + public function getLogoutPath($key) + { + return $this->helper->getLogoutPath($key); + } + + /** + * Generate the absolute logout URL for the firewall. + * + * @param string $key The firewall key + * @return string The absolute logout URL + */ + public function getLogoutUrl($key) + { + return $this->helper->getLogoutUrl($key); + } + + /** + * @see Twig_ExtensionInterface::getName() + */ + public function getName() + { + return 'logout_url'; + } +}