Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ad64124

Browse filesBrowse files
committed
feature #41390 [HttpKernel] Add session cookie handling in cli context (alexander-schranz, Nyholm)
This PR was squashed before being merged into the 5.4 branch. Discussion ---------- [HttpKernel] Add session cookie handling in cli context | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | no | New feature? | no <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tickets | Fix #42890 | License | MIT | Doc PR | symfony/symfony-docs#... <!-- required for new features --> Currently the session cookie is never set on the `request` object. In a normal webserver setup this is not needed as a session is in php-fpm or apache fastcgi written by php itself when the response is outputted in: https://github.com/symfony/symfony/blob/744b901750c1cc09e85bcb2ebdf0505449a1158f/src/Symfony/Component/HttpFoundation/Response.php#L393 When use a runner like `Roadrunner` the `session` cookie is never set on the `Request` object, as this kind of appserver never really outputs something and is running in the `cli` context. Actually there is no way from outside to know if `$session->save()` was called or not because when the code in `Roadrunner` executes the session is actually written and so closed here: https://github.com/symfony/symfony/blob/744b901750c1cc09e85bcb2ebdf0505449a1158f/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php#L279. The best way so to fix this issue is that in CLI context symfony writes the session cookie on the `Response` object when the session is really saved. This fixes issues which we have in the current roadrunner implementation of php runtime: - [x] https://github.com/php-runtime/roadrunner-symfony-nyholm/blob/58665237ef2bff08a1ebf269e64abbf8cd09fbd7/src/Runner.php#L89 - [x] https://github.com/php-runtime/roadrunner-symfony-nyholm/blob/58665237ef2bff08a1ebf269e64abbf8cd09fbd7/src/Runner.php#L81 Beside Roadrunner there is also Swoole implementation which would require the same here: php-runtime/runtime#60 With this changes the Runners can be simplified a lot: https://github.com/php-runtime/runtime/pull/62/files# ## TODO Reset session variables after successfully response. The following code currently lives also in the runtime implementation but is requires also by all runners and so we should find a place where we put it: ``` if (PHP_SESSION_ACTIVE === session_status()) { session_abort(); } // reset all session variables to initialize state $_SESSION = []; session_id(''); // in this case session_start() will generate us a new session_id() ``` EDIT: Added this now to the `AbstractSessionListener` service via `ResetInterface`. Commits ------- b3e4f66 [HttpKernel] Add session cookie handling in cli context
2 parents 47385e5 + b3e4f66 commit ad64124
Copy full SHA for ad64124

File tree

10 files changed

+179
-13
lines changed
Filter options

10 files changed

+179
-13
lines changed

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ public function load(array $configs, ContainerBuilder $container)
327327
$this->sessionConfigEnabled = true;
328328
$this->registerSessionConfiguration($config['session'], $container, $loader);
329329
if (!empty($config['test'])) {
330-
$container->getDefinition('test.session.listener')->setArgument(1, '%session.storage.options%');
330+
$container->getDefinition('test.session.listener')->setArgument(2, '%session.storage.options%');
331331
}
332332
}
333333

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,10 @@
153153
'session_collector' => service('data_collector.request.session_collector')->ignoreOnInvalid(),
154154
]),
155155
param('kernel.debug'),
156+
param('session.storage.options'),
156157
])
157158
->tag('kernel.event_subscriber')
159+
->tag('kernel.reset', ['method' => 'reset'])
158160

159161
// for BC
160162
->alias('session.storage.filesystem', 'session.storage.mock_file')

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/test.php
+4-2Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
use Symfony\Component\BrowserKit\CookieJar;
1717
use Symfony\Component\BrowserKit\History;
1818
use Symfony\Component\DependencyInjection\ServiceLocator;
19-
use Symfony\Component\HttpKernel\EventListener\TestSessionListener;
19+
use Symfony\Component\HttpKernel\EventListener\SessionListener;
2020

2121
return static function (ContainerConfigurator $container) {
2222
$container->parameters()->set('test.client.parameters', []);
@@ -35,11 +35,13 @@
3535
->set('test.client.history', History::class)->share(false)
3636
->set('test.client.cookiejar', CookieJar::class)->share(false)
3737

38-
->set('test.session.listener', TestSessionListener::class)
38+
->set('test.session.listener', SessionListener::class)
3939
->args([
4040
service_locator([
4141
'session' => service('.session.do-not-use')->ignoreOnInvalid(),
4242
]),
43+
param('kernel.debug'),
44+
param('session.storage.options'),
4345
])
4446
->tag('kernel.event_subscriber')
4547

‎src/Symfony/Component/HttpFoundation/Request.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpFoundation/Request.php
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ class Request
186186
protected $format;
187187

188188
/**
189-
* @var SessionInterface|callable
189+
* @var SessionInterface|callable(): SessionInterface
190190
*/
191191
protected $session;
192192

@@ -775,6 +775,8 @@ public function setSession(SessionInterface $session)
775775

776776
/**
777777
* @internal
778+
*
779+
* @param callable(): SessionInterface $factory
778780
*/
779781
public function setSessionFactory(callable $factory)
780782
{

‎src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php
+89-3Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@
1313

1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
16+
use Symfony\Component\HttpFoundation\Cookie;
1617
use Symfony\Component\HttpFoundation\Session\Session;
1718
use Symfony\Component\HttpFoundation\Session\SessionInterface;
19+
use Symfony\Component\HttpFoundation\Session\SessionUtils;
1820
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
1921
use Symfony\Component\HttpKernel\Event\RequestEvent;
2022
use Symfony\Component\HttpKernel\Event\ResponseEvent;
2123
use Symfony\Component\HttpKernel\Exception\UnexpectedSessionUsageException;
2224
use Symfony\Component\HttpKernel\KernelEvents;
25+
use Symfony\Contracts\Service\ResetInterface;
2326

2427
/**
2528
* Sets the session onto the request on the "kernel.request" event and saves
@@ -36,18 +39,24 @@
3639
*
3740
* @internal
3841
*/
39-
abstract class AbstractSessionListener implements EventSubscriberInterface
42+
abstract class AbstractSessionListener implements EventSubscriberInterface, ResetInterface
4043
{
4144
public const NO_AUTO_CACHE_CONTROL_HEADER = 'Symfony-Session-NoAutoCacheControl';
4245

4346
protected $container;
4447
private $sessionUsageStack = [];
4548
private $debug;
4649

47-
public function __construct(ContainerInterface $container = null, bool $debug = false)
50+
/**
51+
* @var array<string, mixed>
52+
*/
53+
private $sessionOptions;
54+
55+
public function __construct(ContainerInterface $container = null, bool $debug = false, array $sessionOptions = [])
4856
{
4957
$this->container = $container;
5058
$this->debug = $debug;
59+
$this->sessionOptions = $sessionOptions;
5160
}
5261

5362
public function onKernelRequest(RequestEvent $event)
@@ -60,7 +69,22 @@ public function onKernelRequest(RequestEvent $event)
6069
if (!$request->hasSession()) {
6170
// This variable prevents calling `$this->getSession()` twice in case the Request (and the below factory) is cloned
6271
$sess = null;
63-
$request->setSessionFactory(function () use (&$sess) { return $sess ?? $sess = $this->getSession(); });
72+
$request->setSessionFactory(function () use (&$sess, $request) {
73+
if (!$sess) {
74+
$sess = $this->getSession();
75+
}
76+
77+
/*
78+
* For supporting sessions in php runtime with runners like roadrunner or swoole the session
79+
* cookie need read from the cookie bag and set on the session storage.
80+
*/
81+
if ($sess && !$sess->isStarted()) {
82+
$sessionId = $request->cookies->get($sess->getName(), '');
83+
$sess->setId($sessionId);
84+
}
85+
86+
return $sess;
87+
});
6488
}
6589

6690
$session = $this->container && $this->container->has('initialized_session') ? $this->container->get('initialized_session') : null;
@@ -109,6 +133,54 @@ public function onKernelResponse(ResponseEvent $event)
109133
* it is saved will just restart it.
110134
*/
111135
$session->save();
136+
137+
/*
138+
* For supporting sessions in php runtime with runners like roadrunner or swoole the session
139+
* cookie need to be written on the response object and should not be written by PHP itself.
140+
*/
141+
$sessionName = $session->getName();
142+
$sessionId = $session->getId();
143+
$sessionCookiePath = $this->sessionOptions['cookie_path'] ?? '/';
144+
$sessionCookieDomain = $this->sessionOptions['cookie_domain'] ?? null;
145+
$sessionCookieSecure = $this->sessionOptions['cookie_secure'] ?? false;
146+
$sessionCookieHttpOnly = $this->sessionOptions['cookie_httponly'] ?? true;
147+
$sessionCookieSameSite = $this->sessionOptions['cookie_samesite'] ?? Cookie::SAMESITE_LAX;
148+
149+
SessionUtils::popSessionCookie($sessionName, $sessionCookiePath);
150+
151+
$request = $event->getRequest();
152+
$requestSessionCookieId = $request->cookies->get($sessionName);
153+
154+
if ($requestSessionCookieId && $session->isEmpty()) {
155+
$response->headers->clearCookie(
156+
$sessionName,
157+
$sessionCookiePath,
158+
$sessionCookieDomain,
159+
$sessionCookieSecure,
160+
$sessionCookieHttpOnly,
161+
$sessionCookieSameSite
162+
);
163+
} elseif ($sessionId !== $requestSessionCookieId) {
164+
$expire = 0;
165+
$lifetime = $this->sessionOptions['cookie_lifetime'] ?? null;
166+
if ($lifetime) {
167+
$expire = time() + $lifetime;
168+
}
169+
170+
$response->headers->setCookie(
171+
Cookie::create(
172+
$sessionName,
173+
$sessionId,
174+
$expire,
175+
$sessionCookiePath,
176+
$sessionCookieDomain,
177+
$sessionCookieSecure,
178+
$sessionCookieHttpOnly,
179+
false,
180+
$sessionCookieSameSite
181+
)
182+
);
183+
}
112184
}
113185

114186
if ($session instanceof Session ? $session->getUsageIndex() === end($this->sessionUsageStack) : !$session->isStarted()) {
@@ -188,6 +260,20 @@ public static function getSubscribedEvents(): array
188260
];
189261
}
190262

263+
public function reset(): void
264+
{
265+
if (\PHP_SESSION_ACTIVE === session_status()) {
266+
session_abort();
267+
}
268+
269+
session_unset();
270+
$_SESSION = [];
271+
272+
if (!headers_sent()) { // session id can only be reset when no headers were so we check for headers_sent first
273+
session_id('');
274+
}
275+
}
276+
191277
/**
192278
* Gets the session object.
193279
*

‎src/Symfony/Component/HttpKernel/EventListener/AbstractTestSessionListener.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/EventListener/AbstractTestSessionListener.php
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
* @author Fabien Potencier <fabien@symfony.com>
2929
*
3030
* @internal
31+
*
32+
* @deprecated the TestSessionListener use the default SessionListener instead
3133
*/
3234
abstract class AbstractTestSessionListener implements EventSubscriberInterface
3335
{
@@ -37,6 +39,8 @@ abstract class AbstractTestSessionListener implements EventSubscriberInterface
3739
public function __construct(array $sessionOptions = [])
3840
{
3941
$this->sessionOptions = $sessionOptions;
42+
43+
trigger_deprecation('symfony/http-kernel', '5.4', 'The %s is deprecated use the %s instead.', __CLASS__, AbstractSessionListener::class);
4044
}
4145

4246
public function onKernelRequest(RequestEvent $event)

‎src/Symfony/Component/HttpKernel/EventListener/SessionListener.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/EventListener/SessionListener.php
-6Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Symfony\Component\HttpKernel\EventListener;
1313

14-
use Psr\Container\ContainerInterface;
1514
use Symfony\Component\HttpFoundation\Session\SessionInterface;
1615
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
1716
use Symfony\Component\HttpKernel\Event\RequestEvent;
@@ -29,11 +28,6 @@
2928
*/
3029
class SessionListener extends AbstractSessionListener
3130
{
32-
public function __construct(ContainerInterface $container, bool $debug = false)
33-
{
34-
parent::__construct($container, $debug);
35-
}
36-
3731
public function onKernelRequest(RequestEvent $event)
3832
{
3933
parent::onKernelRequest($event);

‎src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/EventListener/TestSessionListener.php
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
* @author Fabien Potencier <fabien@symfony.com>
2121
*
2222
* @final
23+
*
24+
* @deprecated the TestSessionListener use the default SessionListener instead
2325
*/
2426
class TestSessionListener extends AbstractTestSessionListener
2527
{

‎src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php
+73Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public function testOnlyTriggeredOnMainRequest()
4747
public function testSessionIsSet()
4848
{
4949
$session = $this->createMock(Session::class);
50+
$session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID');
5051

5152
$requestStack = $this->createMock(RequestStack::class);
5253
$requestStack->expects($this->once())->method('getMainRequest')->willReturn(null);
@@ -73,6 +74,7 @@ public function testSessionIsSet()
7374
public function testSessionUsesFactory()
7475
{
7576
$session = $this->createMock(Session::class);
77+
$session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID');
7678
$sessionFactory = $this->createMock(SessionFactory::class);
7779
$sessionFactory->expects($this->once())->method('createSession')->willReturn($session);
7880

@@ -142,6 +144,32 @@ public function testResponseIsStillPublicIfSessionStartedAndHeaderPresent()
142144
$this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER));
143145
}
144146

147+
public function testSessionSaveAndResponseHasSessionCookie()
148+
{
149+
$session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock();
150+
$session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1));
151+
$session->expects($this->exactly(1))->method('getId')->willReturn('123456');
152+
$session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID');
153+
$session->expects($this->exactly(1))->method('save');
154+
$session->expects($this->exactly(1))->method('isStarted')->willReturn(true);
155+
156+
$container = new Container();
157+
$container->set('initialized_session', $session);
158+
159+
$listener = new SessionListener($container);
160+
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->disableOriginalConstructor()->getMock();
161+
162+
$request = new Request();
163+
$listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST));
164+
165+
$response = new Response();
166+
$listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MASTER_REQUEST, $response));
167+
168+
$cookies = $response->headers->getCookies();
169+
$this->assertSame('PHPSESSID', $cookies[0]->getName());
170+
$this->assertSame('123456', $cookies[0]->getValue());
171+
}
172+
145173
public function testUninitializedSession()
146174
{
147175
$kernel = $this->createMock(HttpKernelInterface::class);
@@ -166,6 +194,7 @@ public function testUninitializedSession()
166194
public function testSurrogateMainRequestIsPublic()
167195
{
168196
$session = $this->createMock(Session::class);
197+
$session->expects($this->exactly(2))->method('getName')->willReturn('PHPSESSID');
169198
$session->expects($this->exactly(4))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1, 1, 1));
170199

171200
$container = new Container();
@@ -205,6 +234,7 @@ public function testSurrogateMainRequestIsPublic()
205234
public function testGetSessionIsCalledOnce()
206235
{
207236
$session = $this->createMock(Session::class);
237+
$session->expects($this->exactly(2))->method('getName')->willReturn('PHPSESSID');
208238
$sessionStorage = $this->createMock(NativeSessionStorage::class);
209239
$kernel = $this->createMock(KernelInterface::class);
210240

@@ -282,6 +312,7 @@ public function testSessionUsageLogIfStatelessAndSessionUsed()
282312
public function testSessionIsSavedWhenUnexpectedSessionExceptionThrown()
283313
{
284314
$session = $this->createMock(Session::class);
315+
$session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID');
285316
$session->method('isStarted')->willReturn(true);
286317
$session->expects($this->exactly(2))->method('getUsageIndex')->will($this->onConsecutiveCalls(0, 1));
287318
$session->expects($this->exactly(1))->method('save');
@@ -368,4 +399,46 @@ public function testSessionUsageCallbackWhenNoStateless()
368399

369400
(new SessionListener($container, true))->onSessionUsage();
370401
}
402+
403+
/**
404+
* @runInSeparateProcess
405+
*/
406+
public function testReset()
407+
{
408+
session_start();
409+
$_SESSION['test'] = ['test'];
410+
session_write_close();
411+
412+
$this->assertNotEmpty($_SESSION);
413+
$this->assertNotEmpty(session_id());
414+
415+
$container = new Container();
416+
417+
(new SessionListener($container, true))->reset();
418+
419+
$this->assertEmpty($_SESSION);
420+
$this->assertEmpty(session_id());
421+
$this->assertSame(\PHP_SESSION_NONE, session_status());
422+
}
423+
424+
/**
425+
* @runInSeparateProcess
426+
*/
427+
public function testResetUnclosedSession()
428+
{
429+
session_start();
430+
$_SESSION['test'] = ['test'];
431+
432+
$this->assertNotEmpty($_SESSION);
433+
$this->assertNotEmpty(session_id());
434+
$this->assertSame(\PHP_SESSION_ACTIVE, session_status());
435+
436+
$container = new Container();
437+
438+
(new SessionListener($container, true))->reset();
439+
440+
$this->assertEmpty($_SESSION);
441+
$this->assertEmpty(session_id());
442+
$this->assertSame(\PHP_SESSION_NONE, session_status());
443+
}
371444
}

‎src/Symfony/Component/HttpKernel/Tests/EventListener/TestSessionListenerTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Tests/EventListener/TestSessionListenerTest.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* Tests SessionListener.
2929
*
3030
* @author Bulat Shakirzyanov <mallluhuct@gmail.com>
31+
* @group legacy
3132
*/
3233
class TestSessionListenerTest extends TestCase
3334
{

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.