diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 44c37e270946a..f6f80c023fbaa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -325,7 +325,7 @@ public function load(array $configs, ContainerBuilder $container) $this->sessionConfigEnabled = true; $this->registerSessionConfiguration($config['session'], $container, $loader); if (!empty($config['test'])) { - $container->getDefinition('test.session.listener')->setArgument(1, '%session.storage.options%'); + $container->getDefinition('test.session.listener')->setArgument(2, '%session.storage.options%'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php index ee9913408bf4e..43c0000dded40 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php @@ -153,8 +153,10 @@ 'session_collector' => service('data_collector.request.session_collector')->ignoreOnInvalid(), ]), param('kernel.debug'), + param('session.storage.options'), ]) ->tag('kernel.event_subscriber') + ->tag('kernel.reset', ['method' => 'reset']) // for BC ->alias('session.storage.filesystem', 'session.storage.mock_file') diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php index 61e4052521329..cd5055eb96863 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php @@ -16,7 +16,7 @@ use Symfony\Component\BrowserKit\CookieJar; use Symfony\Component\BrowserKit\History; use Symfony\Component\DependencyInjection\ServiceLocator; -use Symfony\Component\HttpKernel\EventListener\TestSessionListener; +use Symfony\Component\HttpKernel\EventListener\SessionListener; return static function (ContainerConfigurator $container) { $container->parameters()->set('test.client.parameters', []); @@ -35,11 +35,13 @@ ->set('test.client.history', History::class)->share(false) ->set('test.client.cookiejar', CookieJar::class)->share(false) - ->set('test.session.listener', TestSessionListener::class) + ->set('test.session.listener', SessionListener::class) ->args([ service_locator([ 'session' => service('.session.do-not-use')->ignoreOnInvalid(), ]), + param('kernel.debug'), + param('session.storage.options'), ]) ->tag('kernel.event_subscriber') diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index 2a9741e89e31a..bbfb2360ff389 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -186,7 +186,7 @@ class Request protected $format; /** - * @var SessionInterface|callable + * @var SessionInterface|callable(): SessionInterface */ protected $session; @@ -775,6 +775,8 @@ public function setSession(SessionInterface $session) /** * @internal + * + * @param callable(): SessionInterface $factory */ public function setSessionFactory(callable $factory) { diff --git a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php index 2bbee4a6d41d2..0867cad073dea 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php @@ -13,13 +13,16 @@ use Psr\Container\ContainerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\HttpFoundation\Session\SessionUtils; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Exception\UnexpectedSessionUsageException; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Contracts\Service\ResetInterface; /** * Sets the session onto the request on the "kernel.request" event and saves @@ -36,7 +39,7 @@ * * @internal */ -abstract class AbstractSessionListener implements EventSubscriberInterface +abstract class AbstractSessionListener implements EventSubscriberInterface, ResetInterface { public const NO_AUTO_CACHE_CONTROL_HEADER = 'Symfony-Session-NoAutoCacheControl'; @@ -44,10 +47,16 @@ abstract class AbstractSessionListener implements EventSubscriberInterface private $sessionUsageStack = []; private $debug; - public function __construct(ContainerInterface $container = null, bool $debug = false) + /** + * @var array + */ + private $sessionOptions; + + public function __construct(ContainerInterface $container = null, bool $debug = false, array $sessionOptions = []) { $this->container = $container; $this->debug = $debug; + $this->sessionOptions = $sessionOptions; } public function onKernelRequest(RequestEvent $event) @@ -60,7 +69,22 @@ public function onKernelRequest(RequestEvent $event) if (!$request->hasSession()) { // This variable prevents calling `$this->getSession()` twice in case the Request (and the below factory) is cloned $sess = null; - $request->setSessionFactory(function () use (&$sess) { return $sess ?? $sess = $this->getSession(); }); + $request->setSessionFactory(function () use (&$sess, $request) { + if (!$sess) { + $sess = $this->getSession(); + } + + /* + * For supporting sessions in php runtime with runners like roadrunner or swoole the session + * cookie need read from the cookie bag and set on the session storage. + */ + if ($sess && !$sess->isStarted()) { + $sessionId = $request->cookies->get($sess->getName(), ''); + $sess->setId($sessionId); + } + + return $sess; + }); } $session = $this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : null; @@ -109,6 +133,54 @@ public function onKernelResponse(ResponseEvent $event) * it is saved will just restart it. */ $session->save(); + + /* + * For supporting sessions in php runtime with runners like roadrunner or swoole the session + * cookie need to be written on the response object and should not be written by PHP itself. + */ + $sessionName = $session->getName(); + $sessionId = $session->getId(); + $sessionCookiePath = $this->sessionOptions['cookie_path'] ?? '/'; + $sessionCookieDomain = $this->sessionOptions['cookie_domain'] ?? null; + $sessionCookieSecure = $this->sessionOptions['cookie_secure'] ?? false; + $sessionCookieHttpOnly = $this->sessionOptions['cookie_httponly'] ?? true; + $sessionCookieSameSite = $this->sessionOptions['cookie_samesite'] ?? Cookie::SAMESITE_LAX; + + SessionUtils::popSessionCookie($sessionName, $sessionCookiePath); + + $request = $event->getRequest(); + $requestSessionCookieId = $request->cookies->get($sessionName); + + if ($requestSessionCookieId && $session->isEmpty()) { + $response->headers->clearCookie( + $sessionName, + $sessionCookiePath, + $sessionCookieDomain, + $sessionCookieSecure, + $sessionCookieHttpOnly, + $sessionCookieSameSite + ); + } elseif ($sessionId !== $requestSessionCookieId) { + $expire = 0; + $lifetime = $this->sessionOptions['cookie_lifetime'] ?? null; + if ($lifetime) { + $expire = time() + $lifetime; + } + + $response->headers->setCookie( + Cookie::create( + $sessionName, + $sessionId, + $expire, + $sessionCookiePath, + $sessionCookieDomain, + $sessionCookieSecure, + $sessionCookieHttpOnly, + false, + $sessionCookieSameSite + ) + ); + } } if ($session instanceof Session ? $session->getUsageIndex() === end($this->sessionUsageStack) : !$session->isStarted()) { @@ -188,6 +260,20 @@ public static function getSubscribedEvents(): array ]; } + public function reset(): void + { + if (\PHP_SESSION_ACTIVE === session_status()) { + session_abort(); + } + + session_unset(); + $_SESSION = []; + + if (!headers_sent()) { // session id can only be reset when no headers were so we check for headers_sent first + session_id(''); + } + } + /** * Gets the session object. * diff --git a/src/Symfony/Component/HttpKernel/EventListener/AbstractTestSessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/AbstractTestSessionListener.php index cc091cc5b9bdd..157d50a199394 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/AbstractTestSessionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/AbstractTestSessionListener.php @@ -28,6 +28,8 @@ * @author Fabien Potencier * * @internal + * + * @deprecated the TestSessionListener use the default SessionListener instead */ abstract class AbstractTestSessionListener implements EventSubscriberInterface { @@ -37,6 +39,8 @@ abstract class AbstractTestSessionListener implements EventSubscriberInterface public function __construct(array $sessionOptions = []) { $this->sessionOptions = $sessionOptions; + + trigger_deprecation('symfony/http-kernel', '5.4', 'The %s is deprecated use the %s instead.', __CLASS__, AbstractSessionListener::class); } public function onKernelRequest(RequestEvent $event) diff --git a/src/Symfony/Component/HttpKernel/EventListener/SessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/SessionListener.php index f41939bade11a..61887fde68f14 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/SessionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/SessionListener.php @@ -11,7 +11,6 @@ namespace Symfony\Component\HttpKernel\EventListener; -use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpKernel\Event\RequestEvent; @@ -29,11 +28,6 @@ */ class SessionListener extends AbstractSessionListener { - public function __construct(ContainerInterface $container, bool $debug = false) - { - parent::__construct($container, $debug); - } - public function onKernelRequest(RequestEvent $event) { parent::onKernelRequest($event); diff --git a/src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php index ceac3dde8102b..c5308269c4c05 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php @@ -20,6 +20,8 @@ * @author Fabien Potencier * * @final + * + * @deprecated the TestSessionListener use the default SessionListener instead */ class TestSessionListener extends AbstractTestSessionListener { diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php index d9c272b0d9dc8..d82aba64513e4 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php @@ -47,6 +47,7 @@ public function testOnlyTriggeredOnMainRequest() public function testSessionIsSet() { $session = $this->createMock(Session::class); + $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); $requestStack = $this->createMock(RequestStack::class); $requestStack->expects($this->once())->method('getMainRequest')->willReturn(null); @@ -73,6 +74,7 @@ public function testSessionIsSet() public function testSessionUsesFactory() { $session = $this->createMock(Session::class); + $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); @@ -142,6 +144,32 @@ public function testResponseIsStillPublicIfSessionStartedAndHeaderPresent() $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); } + public function testSessionSaveAndResponseHasSessionCookie() + { + $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); + $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); + $session->expects($this->exactly(1))->method('getId')->willReturn('123456'); + $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); + $session->expects($this->exactly(1))->method('save'); + $session->expects($this->exactly(1))->method('isStarted')->willReturn(true); + + $container = new Container(); + $container->set('initialized_session', $session); + + $listener = new SessionListener($container); + $kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock(); + + $request = new Request(); + $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST)); + + $response = new Response(); + $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response)); + + $cookies = $response->headers->getCookies(); + $this->assertSame('PHPSESSID', $cookies[0]->getName()); + $this->assertSame('123456', $cookies[0]->getValue()); + } + public function testUninitializedSession() { $kernel = $this->createMock(HttpKernelInterface::class); @@ -166,6 +194,7 @@ public function testUninitializedSession() public function testSurrogateMainRequestIsPublic() { $session = $this->createMock(Session::class); + $session->expects($this->exactly(2))->method('getName')->willReturn('PHPSESSID'); $session->expects($this->exactly(4))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1, 1, 1)); $container = new Container(); @@ -205,6 +234,7 @@ public function testSurrogateMainRequestIsPublic() public function testGetSessionIsCalledOnce() { $session = $this->createMock(Session::class); + $session->expects($this->exactly(2))->method('getName')->willReturn('PHPSESSID'); $sessionStorage = $this->createMock(NativeSessionStorage::class); $kernel = $this->createMock(KernelInterface::class); @@ -282,6 +312,7 @@ public function testSessionUsageLogIfStatelessAndSessionUsed() public function testSessionIsSavedWhenUnexpectedSessionExceptionThrown() { $session = $this->createMock(Session::class); + $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); $session->method('isStarted')->willReturn(true); $session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1)); $session->expects($this->exactly(1))->method('save'); @@ -368,4 +399,46 @@ public function testSessionUsageCallbackWhenNoStateless() (new SessionListener($container, true))->onSessionUsage(); } + + /** + * @runInSeparateProcess + */ + public function testReset() + { + session_start(); + $_SESSION['test'] = ['test']; + session_write_close(); + + $this->assertNotEmpty($_SESSION); + $this->assertNotEmpty(session_id()); + + $container = new Container(); + + (new SessionListener($container, true))->reset(); + + $this->assertEmpty($_SESSION); + $this->assertEmpty(session_id()); + $this->assertSame(\PHP_SESSION_NONE, session_status()); + } + + /** + * @runInSeparateProcess + */ + public function testResetUnclosedSession() + { + session_start(); + $_SESSION['test'] = ['test']; + + $this->assertNotEmpty($_SESSION); + $this->assertNotEmpty(session_id()); + $this->assertSame(\PHP_SESSION_ACTIVE, session_status()); + + $container = new Container(); + + (new SessionListener($container, true))->reset(); + + $this->assertEmpty($_SESSION); + $this->assertEmpty(session_id()); + $this->assertSame(\PHP_SESSION_NONE, session_status()); + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/TestSessionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/TestSessionListenerTest.php index abb13bcb10408..3bb76970621c3 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/TestSessionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/TestSessionListenerTest.php @@ -28,6 +28,7 @@ * Tests SessionListener. * * @author Bulat Shakirzyanov + * @group legacy */ class TestSessionListenerTest extends TestCase {