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

Cache limiter support attempt #12

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
789082c
Inject cache headers based on cache_limiter value
pine3ree Apr 26, 2018
4d4ff38
pushing tests to upstream
pine3ree Apr 26, 2018
1925420
remove redundant assignments
pine3ree Apr 26, 2018
1335f8d
add space after not operator
pine3ree Apr 26, 2018
c62d364
change generateCacheHeaders signature
pine3ree Apr 26, 2018
7945a4e
add return types + change getLastModified() signature to accept filen…
pine3ree Apr 26, 2018
38c42b8
added test for empty cache_limiter
pine3ree Apr 26, 2018
b75f95b
added last-modified test
pine3ree Apr 26, 2018
2649c26
added last-modified failure test
pine3ree Apr 26, 2018
bb74817
added unsupported cache_limiter value test
pine3ree Apr 26, 2018
726f701
update getLastModified method: if is_file can do filemtime
pine3ree Apr 26, 2018
7506e66
fix last coverage failure: condition is never met (cacheLimiter is ch…
pine3ree Apr 26, 2018
e08ebe0
add supported limiters property and de-uglify code
pine3ree Apr 26, 2018
90114ef
fix cs space after not
pine3ree Apr 26, 2018
c936c6d
Implement changes requested by the reviewer (good man)
pine3ree Apr 26, 2018
08e26a4
remove unused argument: changed signature
pine3ree Apr 26, 2018
fd89643
re-add return type anotation and re-trigger travis tests
pine3ree Apr 26, 2018
27423f4
trigger travis tests
pine3ree Apr 26, 2018
5ab6ed7
remove redundant retun and trigger new travis test
pine3ree Apr 27, 2018
2fec15d
fix test cases for compatibility with recently merged fix
pine3ree Apr 27, 2018
775598a
add and use helper methods to improve test cases readability
pine3ree Apr 27, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 134 additions & 1 deletion src/PhpSessionPersistence.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@

use function array_merge;
use function bin2hex;
use function filemtime;
use function gmdate;
use function ini_get;
use function random_bytes;
use function session_id;
use function session_name;
use function session_start;
use function session_write_close;
use function sprintf;
use function time;

/**
* Session persistence using ext-session.
Expand All @@ -44,8 +48,49 @@ class PhpSessionPersistence implements SessionPersistenceInterface
/** @var Cookie */
private $cookie;

/** @var string */
private $cacheLimiter;

/** @var int */
private $cacheExpire;

/** @var string */
private $scriptFile;

/** @var array */
private static $supported_cache_limiters = [
'nocache' => true,
'public' => true,
'private' => true,
'private_no_expire' => true,
];

/**
* This unusual past date value is taken from the php-engine source code and
* used "as is" for consistency.
*/
public const CACHE_PAST_DATE = 'Thu, 19 Nov 1981 08:52:00 GMT';

public const HTTP_DATE_FORMAT = 'D, d M Y H:i:s T';

/**
* Memoize session ini settings before starting the request.
*
* The cache_limiter setting is actually "stolen", as we will start the
* session with a forced empty value in order to instruct the php engine to
* skip sending the cache headers (this being php's default behaviour).
* Those headers will be added programmatically to the response along with
* the session set-cookie header when the session data is persisted.
*/
public function __construct()
{
$this->cacheLimiter = ini_get('session.cache_limiter');
$this->cacheExpire = (int) ini_get('session.cache_expire');
}

public function initializeSessionFromRequest(ServerRequestInterface $request) : SessionInterface
{
$this->scriptFile = $request->getServerParams()['SCRIPT_FILENAME'] ?? __FILE__;
$this->cookie = FigRequestCookies::get($request, session_name())->getValue();
$id = $this->cookie ?: $this->generateSessionId();
$this->startSession($id);
Expand All @@ -66,7 +111,18 @@ public function persistSession(SessionInterface $session, ResponseInterface $res
->withValue(session_id())
->withPath(ini_get('session.cookie_path'));

return FigResponseCookies::set($response, $sessionCookie);
$response = FigResponseCookies::set($response, $sessionCookie);

if (! $this->cacheLimiter || $this->responseAlreadyHasCacheHeaders($response)) {
return $response;
}

$cacheHeaders = $this->generateCacheHeaders();
foreach ($cacheHeaders as $name => $value) {
if (false !== $value) {
$response = $response->withHeader($name, $value);
}
}
}

return $response;
Expand All @@ -81,6 +137,7 @@ private function startSession(string $id, array $options = []) : void
session_start(array_merge([
'use_cookies' => false,
'use_only_cookies' => true,
'cache_limiter' => '',
], $options));
}

Expand All @@ -105,4 +162,80 @@ private function generateSessionId() : string
{
return bin2hex(random_bytes(16));
}

/**
* Generate cache http headers for this instance's session cache_limiter and
* cache_expire values
*/
private function generateCacheHeaders() : array
{
// Unsupported cache_limiter
if (! isset(self::$supported_cache_limiters[$this->cacheLimiter])) {
return [];
}

// cache_limiter: 'nocache'
if ('nocache' === $this->cacheLimiter) {
return [
'Expires' => self::CACHE_PAST_DATE,
'Cache-Control' => 'no-store, no-cache, must-revalidate',
'Pragma' => 'no-cache',
];
}

$maxAge = 60 * $this->cacheExpire;
$lastModified = $this->getLastModified();

// cache_limiter: 'public'
if ('public' === $this->cacheLimiter) {
return [
'Expires' => gmdate(self::HTTP_DATE_FORMAT, time() + $maxAge),
'Cache-Control' => sprintf('public, max-age=%d', $maxAge),
'Last-Modified' => $lastModified,
];
}

// cache_limiter: 'private'
if ('private' === $this->cacheLimiter) {
return [
'Expires' => self::CACHE_PAST_DATE,
'Cache-Control' => sprintf('private, max-age=%d', $maxAge),
'Last-Modified' => $lastModified,
];
}

// last possible case, cache_limiter = 'private_no_expire'
return [
'Cache-Control' => sprintf('private, max-age=%d', $maxAge),
'Last-Modified' => $lastModified,
];
}

/**
* Return the Last-Modified header line based on the request's script file
* modified time. If no script file could be derived from the request we use
* this class file modification time as fallback.
* @return string|false
*/
private function getLastModified()
{
if ($this->scriptFile && is_file($this->scriptFile)) {
return gmdate(self::HTTP_DATE_FORMAT, filemtime($this->scriptFile));
}

return false;
}

/**
* Check if the response already carries cache headers
*/
private function responseAlreadyHasCacheHeaders(ResponseInterface $response) : bool
{
return (
$response->hasHeader('Expires')
|| $response->hasHeader('Last-Modified')
|| $response->hasHeader('Cache-Control')
|| $response->hasHeader('Pragma')
);
}
}
Loading