Skip to content
This repository has been archived by the owner on Jan 29, 2020. It is now read-only.

Commit

Permalink
Merge 09e4d31 into 62096b1
Browse files Browse the repository at this point in the history
  • Loading branch information
weierophinney committed Oct 30, 2018
2 parents 62096b1 + 09e4d31 commit b65bc16
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 12 deletions.
2 changes: 1 addition & 1 deletion composer.json
Expand Up @@ -23,7 +23,7 @@
"dflydev/fig-cookies": "^1.0.2 || ^2.0",
"psr/cache": "^1.0",
"psr/container": "^1.0",
"zendframework/zend-expressive-session": "^1.1"
"zendframework/zend-expressive-session": "^1.2"
},
"require-dev": {
"phpunit/phpunit": "^7.1.1",
Expand Down
16 changes: 8 additions & 8 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions docs/book/v1/config.md
Expand Up @@ -119,6 +119,13 @@ return [
// a boolean true value will enable the feature. When enabled, the
// cookie will be generated with an Expires directive equal to the
// the current time plus the cache_expire value as noted above.
//
// As of 1.1.0, developers may define the session TTL by calling the
// session instance's `persistSessionFor(int $duration)` method. When
// this is present, the engine will use that value even if the below
// flag is toggled off. When the flag is enabled, the session TTL will
// be honored if it is greater than zero; otherwise, the cache_expire
// value will be used, as this defines the global default behavior.
'persistent' => false,
],
];
Expand Down
6 changes: 5 additions & 1 deletion docs/book/v1/manual.md
Expand Up @@ -21,7 +21,11 @@ The following details the constructor of the `Zend\Expressive\Session\Cache\Cach
* directory, using the filemtime() of the first found.
* @param bool $persistent Whether or not to create a persistent cookie. If
* provided, this sets the Expires directive for the cookie based on
* the value of $cacheExpire.
* the value of $cacheExpire. Developers can also set the expiry at
* runtime via the Session instance, using its persistSessionFor()
* method; that value will be used in all cases unless it is zero
* and $persistent is toggled to true, in which case the value of
* $cacheExpire will be used.
*/
public function __construct(
\Psr\Cache\CacheItemPoolInterface $cache,
Expand Down
16 changes: 14 additions & 2 deletions src/CacheSessionPersistence.php
Expand Up @@ -18,6 +18,7 @@
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Expressive\Session\Session;
use Zend\Expressive\Session\SessionCookiePersistenceInterface;
use Zend\Expressive\Session\SessionInterface;
use Zend\Expressive\Session\SessionPersistenceInterface;

Expand Down Expand Up @@ -163,9 +164,10 @@ public function persistSession(SessionInterface $session, ResponseInterface $res
->withValue($id)
->withPath($this->cookiePath);

if ($this->persistent) {
$persistenceDuration = $this->getPersistenceDuration($session);
if ($persistenceDuration) {
$sessionCookie = $sessionCookie->withExpires(
(new DateTimeImmutable())->add(new DateInterval(sprintf('PT%dS', $this->cacheExpire)))
(new DateTimeImmutable())->add(new DateInterval(sprintf('PT%dS', $persistenceDuration)))
);
}

Expand Down Expand Up @@ -319,4 +321,14 @@ private function responseAlreadyHasCacheHeaders(ResponseInterface $response) : b
|| $response->hasHeader('Pragma')
);
}

private function getPersistenceDuration(SessionInterface $session) : int
{
$duration = $this->persistent ? $this->cacheExpire : 0;
if ($session instanceof SessionInterface) {
$ttl = $session->getSessionLifetime();
$duration = $ttl ? $ttl : $duration;
}
return $duration < 0 ? 0 : $duration;
}
}
191 changes: 191 additions & 0 deletions test/CacheSessionPersistenceTest.php
Expand Up @@ -20,6 +20,7 @@
use Zend\Expressive\Session\Cache\CacheSessionPersistence;
use Zend\Expressive\Session\Cache\Exception;
use Zend\Expressive\Session\Session;
use Zend\Expressive\Session\SessionCookiePersistenceInterface;

class CacheSessionPersistenceTest extends TestCase
{
Expand Down Expand Up @@ -92,6 +93,24 @@ public function assertCookieExpiryMirrorsExpiry(int $expiry, Response $response)
);
}

public function assertCookieHasNoExpiryDirective(Response $response)
{
$setCookie = $response->getHeaderLine('Set-Cookie');
$parts = explode(';', $setCookie);
$parts = array_map(function ($value) {
return trim($value);
}, $parts);
$parts = array_filter($parts, function ($value) {
return (bool) preg_match('/^Expires=/', $value);
});

$this->assertSame(
0,
count($parts),
'Expires directive found in cookie, but should not be present: ' . $setCookie
);
}

public function assertCacheHeaders(string $cacheLimiter, Response $response)
{
switch ($cacheLimiter) {
Expand Down Expand Up @@ -678,4 +697,176 @@ public function testPersistentSessionCookieIncludesExpiration()
$this->assertNotSame($response, $result);
$this->assertCookieExpiryMirrorsExpiry(600, $result);
}

public function testPersistenceDurationSpecifiedInSessionUsedWhenPresentEvenWhenEngineDoesNotSpecifyPersistence()
{
$session = new Session(['foo' => 'bar'], 'identifier');
$response = new Response();

// Engine created with defaults, which means no cookie persistence
$persistence = new CacheSessionPersistence(
$this->cachePool->reveal(),
'test'
);

$cacheItem = $this->prophesize(CacheItemInterface::class);
$cacheItem
->set(Argument::that(function ($value) {
TestCase::assertInternalType('array', $value);
TestCase::assertArrayHasKey('foo', $value);
TestCase::assertSame('bar', $value['foo']);
TestCase::assertArrayHasKey(SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY, $value);
TestCase::assertSame(1200, $value[SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY]);
return $value;
}))
->shouldBeCalled();
$cacheItem->expiresAfter(Argument::type('int'))->shouldBeCalled();
$this->cachePool->hasItem('identifier')->willReturn(false);
$this->cachePool
->getItem(Argument::that(function ($value) {
TestCase::assertRegExp('/^[a-f0-9]{32}$/', $value);
return $value;
}))
->will([$cacheItem, 'reveal']);
$this->cachePool->save(Argument::that([$cacheItem, 'reveal']))->shouldBeCalled();

$session->persistSessionFor(1200);
$result = $persistence->persistSession($session, $response);

$this->assertNotSame($response, $result);
$this->assertCookieExpiryMirrorsExpiry(1200, $result);
}

public function testPersistenceDurationSpecifiedInSessionOverridesExpiryWhenSessionPersistenceIsEnabled()
{
$session = new Session(['foo' => 'bar'], 'identifier');
$response = new Response();
$persistence = new CacheSessionPersistence(
$this->cachePool->reveal(),
'test',
'/',
'nocache',
600, // expiry
time(),
true // mark session cookie as persistent
);

$cacheItem = $this->prophesize(CacheItemInterface::class);
$cacheItem
->set(Argument::that(function ($value) {
TestCase::assertInternalType('array', $value);
TestCase::assertArrayHasKey('foo', $value);
TestCase::assertSame('bar', $value['foo']);
TestCase::assertArrayHasKey(SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY, $value);
TestCase::assertSame(1200, $value[SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY]);
return $value;
}))
->shouldBeCalled();
$cacheItem->expiresAfter(Argument::type('int'))->shouldBeCalled();
$this->cachePool->hasItem('identifier')->willReturn(false);
$this->cachePool
->getItem(Argument::that(function ($value) {
TestCase::assertRegExp('/^[a-f0-9]{32}$/', $value);
return $value;
}))
->will([$cacheItem, 'reveal']);
$this->cachePool->save(Argument::that([$cacheItem, 'reveal']))->shouldBeCalled();

$session->persistSessionFor(1200);
$result = $persistence->persistSession($session, $response);

$this->assertNotSame($response, $result);
$this->assertCookieExpiryMirrorsExpiry(1200, $result);
}

public function testPersistenceDurationOfZeroSpecifiedInSessionDisablesPersistence()
{
$session = new Session([
'foo' => 'bar',
SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY => 1200,
], 'identifier');
$response = new Response();
$persistence = new CacheSessionPersistence(
$this->cachePool->reveal(),
'test'
);

$cacheItem = $this->prophesize(CacheItemInterface::class);
$cacheItem
->set(Argument::that(function ($value) {
TestCase::assertInternalType('array', $value);
TestCase::assertArrayHasKey('foo', $value);
TestCase::assertSame('bar', $value['foo']);
TestCase::assertArrayHasKey(SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY, $value);
TestCase::assertSame(0, $value[SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY]);
return $value;
}))
->shouldBeCalled();
$cacheItem->expiresAfter(Argument::type('int'))->shouldBeCalled();
$this->cachePool->hasItem('identifier')->willReturn(false);
$this->cachePool
->getItem(Argument::that(function ($value) {
TestCase::assertRegExp('/^[a-f0-9]{32}$/', $value);
return $value;
}))
->will([$cacheItem, 'reveal']);
$this->cachePool->save(Argument::that([$cacheItem, 'reveal']))->shouldBeCalled();

$session->persistSessionFor(0);
$result = $persistence->persistSession($session, $response);

$this->assertNotSame($response, $result);
$this->assertCookieHasNoExpiryDirective($result);
}

/**
* Verify global persistence rules trump local when it comes to expiring a session.
*
* Since zero is the default return value of getSessionLifetime, we should use the
* global default persistence when that value is encountered.
*/
public function testPersistenceDurationOfZeroDoesNotOverrideGlobalPersistenceExpiry()
{
$session = new Session([
'foo' => 'bar',
SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY => 1200,
], 'identifier');
$response = new Response();
$persistence = new CacheSessionPersistence(
$this->cachePool->reveal(),
'test',
'/',
'nocache',
600, // expiry
time(),
true // mark session cookie as persistent
);

$cacheItem = $this->prophesize(CacheItemInterface::class);
$cacheItem
->set(Argument::that(function ($value) {
TestCase::assertInternalType('array', $value);
TestCase::assertArrayHasKey('foo', $value);
TestCase::assertSame('bar', $value['foo']);
TestCase::assertArrayHasKey(SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY, $value);
TestCase::assertSame(0, $value[SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY]);
return $value;
}))
->shouldBeCalled();
$cacheItem->expiresAfter(Argument::type('int'))->shouldBeCalled();
$this->cachePool->hasItem('identifier')->willReturn(false);
$this->cachePool
->getItem(Argument::that(function ($value) {
TestCase::assertRegExp('/^[a-f0-9]{32}$/', $value);
return $value;
}))
->will([$cacheItem, 'reveal']);
$this->cachePool->save(Argument::that([$cacheItem, 'reveal']))->shouldBeCalled();

$session->persistSessionFor(0);
$result = $persistence->persistSession($session, $response);

$this->assertNotSame($response, $result);
$this->assertCookieExpiryMirrorsExpiry(600, $result);
}
}

0 comments on commit b65bc16

Please sign in to comment.