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

Commit c8223cc

Browse files
committed
Merge branch 'feature/12' into develop
Close #12
2 parents 0e394a4 + aea9627 commit c8223cc

File tree

3 files changed

+406
-6
lines changed

3 files changed

+406
-6
lines changed

CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ All notable changes to this project will be documented in this file, in reverse
1010

1111
### Changed
1212

13-
- Nothing.
13+
- [#12](https://github.com/zendframework/zend-expressive-session-ext/pull/12) updates the `PhpSessionPersistence` class such that it is now responsible for
14+
emitting the various cache limiter headers (`Expires`, `Cache-Control`, `Last-Modified`, and `Pragma`) normally
15+
emitted by ext-session and controlled by the `session.cache_limiter` and `session.cache_expire` INI settings.
16+
This approach ensures that those headers are not overwritten by ext-session if set elsewhere in your
17+
application.
1418

1519
### Deprecated
1620

src/PhpSessionPersistence.php

+134-1
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@
1919

2020
use function array_merge;
2121
use function bin2hex;
22+
use function filemtime;
23+
use function gmdate;
2224
use function ini_get;
2325
use function random_bytes;
2426
use function session_id;
2527
use function session_name;
2628
use function session_start;
2729
use function session_write_close;
30+
use function sprintf;
31+
use function time;
2832

2933
/**
3034
* Session persistence using ext-session.
@@ -44,8 +48,49 @@ class PhpSessionPersistence implements SessionPersistenceInterface
4448
/** @var Cookie */
4549
private $cookie;
4650

51+
/** @var string */
52+
private $cacheLimiter;
53+
54+
/** @var int */
55+
private $cacheExpire;
56+
57+
/** @var string */
58+
private $scriptFile;
59+
60+
/** @var array */
61+
private static $supported_cache_limiters = [
62+
'nocache' => true,
63+
'public' => true,
64+
'private' => true,
65+
'private_no_expire' => true,
66+
];
67+
68+
/**
69+
* This unusual past date value is taken from the php-engine source code and
70+
* used "as is" for consistency.
71+
*/
72+
public const CACHE_PAST_DATE = 'Thu, 19 Nov 1981 08:52:00 GMT';
73+
74+
public const HTTP_DATE_FORMAT = 'D, d M Y H:i:s T';
75+
76+
/**
77+
* Memoize session ini settings before starting the request.
78+
*
79+
* The cache_limiter setting is actually "stolen", as we will start the
80+
* session with a forced empty value in order to instruct the php engine to
81+
* skip sending the cache headers (this being php's default behaviour).
82+
* Those headers will be added programmatically to the response along with
83+
* the session set-cookie header when the session data is persisted.
84+
*/
85+
public function __construct()
86+
{
87+
$this->cacheLimiter = ini_get('session.cache_limiter');
88+
$this->cacheExpire = (int) ini_get('session.cache_expire');
89+
}
90+
4791
public function initializeSessionFromRequest(ServerRequestInterface $request) : SessionInterface
4892
{
93+
$this->scriptFile = $request->getServerParams()['SCRIPT_FILENAME'] ?? __FILE__;
4994
$this->cookie = FigRequestCookies::get($request, session_name())->getValue();
5095
$id = $this->cookie ?: $this->generateSessionId();
5196
$this->startSession($id);
@@ -66,7 +111,18 @@ public function persistSession(SessionInterface $session, ResponseInterface $res
66111
->withValue($this->cookie)
67112
->withPath(ini_get('session.cookie_path'));
68113

69-
return FigResponseCookies::set($response, $sessionCookie);
114+
$response = FigResponseCookies::set($response, $sessionCookie);
115+
116+
if (! $this->cacheLimiter || $this->responseAlreadyHasCacheHeaders($response)) {
117+
return $response;
118+
}
119+
120+
$cacheHeaders = $this->generateCacheHeaders();
121+
foreach ($cacheHeaders as $name => $value) {
122+
if (false !== $value) {
123+
$response = $response->withHeader($name, $value);
124+
}
125+
}
70126
}
71127

72128
return $response;
@@ -81,6 +137,7 @@ private function startSession(string $id, array $options = []) : void
81137
session_start(array_merge([
82138
'use_cookies' => false,
83139
'use_only_cookies' => true,
140+
'cache_limiter' => '',
84141
], $options));
85142
}
86143

@@ -105,4 +162,80 @@ private function generateSessionId() : string
105162
{
106163
return bin2hex(random_bytes(16));
107164
}
165+
166+
/**
167+
* Generate cache http headers for this instance's session cache_limiter and
168+
* cache_expire values
169+
*/
170+
private function generateCacheHeaders() : array
171+
{
172+
// Unsupported cache_limiter
173+
if (! isset(self::$supported_cache_limiters[$this->cacheLimiter])) {
174+
return [];
175+
}
176+
177+
// cache_limiter: 'nocache'
178+
if ('nocache' === $this->cacheLimiter) {
179+
return [
180+
'Expires' => self::CACHE_PAST_DATE,
181+
'Cache-Control' => 'no-store, no-cache, must-revalidate',
182+
'Pragma' => 'no-cache',
183+
];
184+
}
185+
186+
$maxAge = 60 * $this->cacheExpire;
187+
$lastModified = $this->getLastModified();
188+
189+
// cache_limiter: 'public'
190+
if ('public' === $this->cacheLimiter) {
191+
return [
192+
'Expires' => gmdate(self::HTTP_DATE_FORMAT, time() + $maxAge),
193+
'Cache-Control' => sprintf('public, max-age=%d', $maxAge),
194+
'Last-Modified' => $lastModified,
195+
];
196+
}
197+
198+
// cache_limiter: 'private'
199+
if ('private' === $this->cacheLimiter) {
200+
return [
201+
'Expires' => self::CACHE_PAST_DATE,
202+
'Cache-Control' => sprintf('private, max-age=%d', $maxAge),
203+
'Last-Modified' => $lastModified,
204+
];
205+
}
206+
207+
// last possible case, cache_limiter = 'private_no_expire'
208+
return [
209+
'Cache-Control' => sprintf('private, max-age=%d', $maxAge),
210+
'Last-Modified' => $lastModified,
211+
];
212+
}
213+
214+
/**
215+
* Return the Last-Modified header line based on the request's script file
216+
* modified time. If no script file could be derived from the request we use
217+
* this class file modification time as fallback.
218+
* @return string|false
219+
*/
220+
private function getLastModified()
221+
{
222+
if ($this->scriptFile && is_file($this->scriptFile)) {
223+
return gmdate(self::HTTP_DATE_FORMAT, filemtime($this->scriptFile));
224+
}
225+
226+
return false;
227+
}
228+
229+
/**
230+
* Check if the response already carries cache headers
231+
*/
232+
private function responseAlreadyHasCacheHeaders(ResponseInterface $response) : bool
233+
{
234+
return (
235+
$response->hasHeader('Expires')
236+
|| $response->hasHeader('Last-Modified')
237+
|| $response->hasHeader('Cache-Control')
238+
|| $response->hasHeader('Pragma')
239+
);
240+
}
108241
}

0 commit comments

Comments
 (0)