19
19
20
20
use function array_merge ;
21
21
use function bin2hex ;
22
+ use function filemtime ;
23
+ use function gmdate ;
22
24
use function ini_get ;
23
25
use function random_bytes ;
24
26
use function session_id ;
25
27
use function session_name ;
26
28
use function session_start ;
27
29
use function session_write_close ;
30
+ use function sprintf ;
31
+ use function time ;
28
32
29
33
/**
30
34
* Session persistence using ext-session.
@@ -44,8 +48,49 @@ class PhpSessionPersistence implements SessionPersistenceInterface
44
48
/** @var Cookie */
45
49
private $ cookie ;
46
50
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
+
47
91
public function initializeSessionFromRequest (ServerRequestInterface $ request ) : SessionInterface
48
92
{
93
+ $ this ->scriptFile = $ request ->getServerParams ()['SCRIPT_FILENAME ' ] ?? __FILE__ ;
49
94
$ this ->cookie = FigRequestCookies::get ($ request , session_name ())->getValue ();
50
95
$ id = $ this ->cookie ?: $ this ->generateSessionId ();
51
96
$ this ->startSession ($ id );
@@ -66,7 +111,18 @@ public function persistSession(SessionInterface $session, ResponseInterface $res
66
111
->withValue ($ this ->cookie )
67
112
->withPath (ini_get ('session.cookie_path ' ));
68
113
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
+ }
70
126
}
71
127
72
128
return $ response ;
@@ -81,6 +137,7 @@ private function startSession(string $id, array $options = []) : void
81
137
session_start (array_merge ([
82
138
'use_cookies ' => false ,
83
139
'use_only_cookies ' => true ,
140
+ 'cache_limiter ' => '' ,
84
141
], $ options ));
85
142
}
86
143
@@ -105,4 +162,80 @@ private function generateSessionId() : string
105
162
{
106
163
return bin2hex (random_bytes (16 ));
107
164
}
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
+ }
108
241
}
0 commit comments