From 15dccc45f392051db3cc601ceda4fd04819928bf Mon Sep 17 00:00:00 2001 From: Arnold Daniels Date: Wed, 15 Apr 2020 23:52:53 +0200 Subject: [PATCH 1/4] Added .gitattributes --- .gitattributes | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1b0faf3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +/tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.scrutinizer.yml export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore +/phpcs.xml.dist export-ignore +/phpstan.neon export-ignore +/README.md export-ignore From eab2381d842fa7d70051ab2ebedade4556240ecb Mon Sep 17 00:00:00 2001 From: Arnold Daniels Date: Thu, 16 Apr 2020 02:00:27 +0200 Subject: [PATCH 2/4] Added `kill()` and `rotate()` methods. Throw `NoSessionException` if there's no session instead of a generic `RuntimeException`. --- README.md | 15 ++++++++- src/GlobalSession.php | 59 ++++++++++++++++++++++++++++++++--- src/MockSession.php | 22 +++++++++++++ src/NoSessionException.php | 12 +++++++ src/SessionInterface.php | 16 ++++++++-- tests/AbstractSessionTest.php | 22 +++++++++++++ tests/GlobalSessionTest.php | 35 +++++++++++++++++++-- 7 files changed, 172 insertions(+), 9 deletions(-) create mode 100644 src/NoSessionException.php diff --git a/README.md b/README.md index 9138340..75f72d1 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,20 @@ if (isset($session['foo.user'])) { } ``` -Use `$session->abort()` to abort writing the changes. Call `$session->clear()` to clear all data from the session. +### Methods +* `start()` - Start the session. +* `status()` - Get the session status. +* `stop()` - Write session data and end session. +* `abort()` - Discard session array changes and finish session. +* `clear()` - Clear all data from the session. +* `kill()` - Destroy the session and remove the session cookie. +* `rotate()` - Delete the current session and start a new one. + +When rotating a session, it's possible to copy some of the data by supplying a callback. + +```php +$session->rotate(fn(array $oldSessionData) => ['tid' => $oldSessionData['tid'] ?? null]); +``` ### Flash diff --git a/src/GlobalSession.php b/src/GlobalSession.php index d29958f..0242409 100644 --- a/src/GlobalSession.php +++ b/src/GlobalSession.php @@ -20,7 +20,7 @@ class GlobalSession implements SessionInterface /** * Session constructor. * - * @param array $options Passed to session_start() + * @param array $options Passed to session_start() * @param FlashBag|null $flashBag */ public function __construct(array $options = [], ?FlashBag $flashBag = null) @@ -29,7 +29,6 @@ public function __construct(array $options = [], ?FlashBag $flashBag = null) $this->flashBag = $flashBag ?? new FlashBag(); } - /** * Start the session. * @see session_start() @@ -81,6 +80,57 @@ public function clear(): void $_SESSION = []; } + /** + * @inheritDoc + */ + public function kill(): void + { + $this->assertStarted(); + + $this->removeCookie(session_name()); + $_SESSION = []; + session_destroy(); + } + + /** + * Remove the session cookie. + * @codeCoverageIgnore + */ + protected function removeCookie(string $name): void + { + if (!(bool)ini_get("session.use_cookies")) { + return; + } + + $options = ['expires' => time() - 42000] + session_get_cookie_params(); + setcookie($name, '', $options); + } + + /** + * @inheritDoc + */ + public function rotate(?callable $copy = null): void + { + $this->assertStarted(); + + $data = isset($copy) ? $copy($_SESSION) : []; + + $_SESSION = []; + $this->regenerateId(true); + + $_SESSION = $data; + } + + /** + * Wrapper around `session_regenerate_id()` + * @codeCoverageIgnore + */ + protected function regenerateId(bool $delete): void + { + session_regenerate_id($delete); + } + + /** * @param string $offset * @return bool @@ -124,15 +174,16 @@ public function offsetUnset($offset): void unset($_SESSION[$offset]); } + /** * Assert that there is an active session. * - * @throws \RuntimeException + * @throws NoSessionException */ protected function assertStarted(): void { if (session_status() !== \PHP_SESSION_ACTIVE) { - throw new \RuntimeException("Session not started"); + throw new NoSessionException("Session not started"); } } } diff --git a/src/MockSession.php b/src/MockSession.php index bc0fb2a..fcabf6b 100644 --- a/src/MockSession.php +++ b/src/MockSession.php @@ -76,6 +76,28 @@ public function clear(): void $this->exchangeArray([]); } + /** + * @inheritDoc + */ + public function kill(): void + { + $this->initialData = []; + $this->exchangeArray([]); + $this->status = \PHP_SESSION_NONE; + } + + /** + * @inheritDoc + */ + public function rotate(?callable $copy = null): void + { + $data = isset($copy) ? $copy($this->getArrayCopy()) : []; + + $this->initialData = []; + $this->exchangeArray($data); + $this->status = \PHP_SESSION_ACTIVE; + } + /** * @param string $offset diff --git a/src/NoSessionException.php b/src/NoSessionException.php new file mode 100644 index 0000000..4143e8a --- /dev/null +++ b/src/NoSessionException.php @@ -0,0 +1,12 @@ +assertSessionData(['foo' => 'bar', 'zoo' => 10]); $this->session->abort(); + $this->assertEquals(PHP_SESSION_NONE, $this->session->status()); + $this->session->start(); + $this->assertEquals(PHP_SESSION_ACTIVE, $this->session->status()); $this->assertSessionData(['foo' => 'bar']); } @@ -50,5 +53,24 @@ public function testClear() $this->session->clear(); $this->assertSessionData([]); + $this->assertEquals(PHP_SESSION_ACTIVE, $this->session->status()); + } + + public function testKill() + { + $this->assertSessionData(['foo' => 'bar']); + + $this->session->kill(); + $this->assertSessionData([]); + $this->assertEquals(PHP_SESSION_NONE, $this->session->status()); + } + + public function testRotate() + { + $this->assertSessionData(['foo' => 'bar']); + + $this->session->rotate(); + $this->assertSessionData([]); + $this->assertEquals(PHP_SESSION_ACTIVE, $this->session->status()); } } diff --git a/tests/GlobalSessionTest.php b/tests/GlobalSessionTest.php index 2db5da0..0aa2b28 100644 --- a/tests/GlobalSessionTest.php +++ b/tests/GlobalSessionTest.php @@ -5,6 +5,7 @@ namespace Jasny\Session\Tests; use Jasny\Session\GlobalSession; +use Jasny\Session\NoSessionException; /** * @covers \Jasny\Session\GlobalSession @@ -15,7 +16,10 @@ public function setUp(): void { $this->createTestSession(); - $this->session = new GlobalSession(); + $this->session = $this->getMockBuilder(GlobalSession::class) + ->onlyMethods(['removeCookie', 'regenerateId']) + ->getMock(); + session_start(); } @@ -62,9 +66,36 @@ public function testAssertStarted() { $this->session->stop(); - $this->expectException(\RuntimeException::class); + $this->expectException(NoSessionException::class); $this->expectExceptionMessage("Session not started"); $this->session['one'] = 1; } + + + public function testKill() + { + $this->session->expects($this->once())->method('removeCookie')->with('PHPSESSID'); + + parent::testKill(); + + $this->assertNotEquals('test', session_id()); + + // Check if the old session is cleared + session_write_close(); + session_id('test'); + session_start(); + + $this->assertEquals([], $_SESSION); + } + + /** + * Unable to test `session_regenerate_id` as it always fails because of headers sent. + */ + public function testRotate() + { + $this->session->expects($this->once())->method('regenerateId'); + + parent::testRotate(); + } } From 964e4ef5ab8c90f51f10a494e67a483f2fad12ff Mon Sep 17 00:00:00 2001 From: Arnold Daniels Date: Fri, 17 Apr 2020 02:04:41 +0200 Subject: [PATCH 3/4] Update README.md [skip ci] --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 75f72d1..ee9e34f 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,12 @@ if (isset($session['foo.user'])) { } ``` +The session is started by the middleware. + ### Methods + +The session object implements `SessionInterface` and has the following methods; + * `start()` - Start the session. * `status()` - Get the session status. * `stop()` - Write session data and end session. @@ -55,6 +60,29 @@ When rotating a session, it's possible to copy some of the data by supplying a c $session->rotate(fn(array $oldSessionData) => ['tid' => $oldSessionData['tid'] ?? null]); ``` +### Session options + +By default, the middleware will create a `GlobalSession` object. This object is linked to PHPs session management including +`$_SESSION`. You can manually instantiate this object, supplying session options. These options are passed to +`session_start()`. + +```php +use Jasny\Session\GlobalSession; +use Jasny\Session\SessionMiddleware; + +$session = new GlobalSession([ + 'cookie_lifetime' => 0, + 'cookie_httponly' => 1, + 'use_only_cookies' => 1, + 'use_trans_sid' => 0, + 'cookie_secure' => (bool)($_SERVER['HTTPS'] ?? false), + 'cookie_samesite' => 'Lax', +]); + +$router->add(new SessionMiddleware($session)); +$response = $router->handle($request); +``` + ### Flash The session flash object can be used to pass a message to the next request. It is automatically removed from the session From bf1d2fc1801fe2b9a70fd6c26350fcfa723a0fcd Mon Sep 17 00:00:00 2001 From: Arnold Daniels Date: Fri, 17 Apr 2020 02:44:18 +0200 Subject: [PATCH 4/4] 'lifetime' isn't a valid cookie option. Unset it when removing the session cookie. --- src/GlobalSession.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/GlobalSession.php b/src/GlobalSession.php index 0242409..4ef9652 100644 --- a/src/GlobalSession.php +++ b/src/GlobalSession.php @@ -103,6 +103,8 @@ protected function removeCookie(string $name): void } $options = ['expires' => time() - 42000] + session_get_cookie_params(); + unset($options['lifetime']); + setcookie($name, '', $options); }