Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 84 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
SessionHeadersHandler | |
0.00% |
0 / 84 |
|
0.00% |
0 / 9 |
870 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
__invoke | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
withNewSessionCookie | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
timestamp | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
withCacheLimiter | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
42 | |||
cacheLimiterPublic | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
cacheLimiterPrivateNoExpire | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
cacheLimiterPrivate | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
cacheLimiterNocache | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace BO\Slim\Middleware; |
4 | |
5 | use Psr\Http\Message\ResponseInterface as Response; |
6 | use Psr\Http\Message\ServerRequestInterface as Request; |
7 | use Psr\Http\Server\RequestHandlerInterface; |
8 | use RuntimeException; |
9 | use BO\Slim\Factory\ResponseFactory; |
10 | |
11 | /** |
12 | * |
13 | * Sends the session headers in the Response, putting them under manual control |
14 | * rather than relying on PHP to send them itself. |
15 | * |
16 | * This works correctly only if you have these settings: |
17 | * |
18 | * ``` |
19 | * ini_set('session.use_trans_sid', false); |
20 | * ini_set('session.use_cookies', false); |
21 | * ini_set('session.use_only_cookies', true); |
22 | * ini_set('session.cache_limiter', ''); |
23 | * ``` |
24 | * |
25 | * Note that the Last-Modified value will not be the last time the session was |
26 | * saved, but instead the current `time()`. |
27 | * |
28 | * |
29 | */ |
30 | class SessionHeadersHandler |
31 | { |
32 | /** |
33 | * The timestamp for "already expired." |
34 | */ |
35 | const EXPIRED = 'Thu, 19 Nov 1981 08:52:00 GMT'; |
36 | |
37 | /** |
38 | * |
39 | * The cache limiter type, if any. |
40 | * |
41 | * @var string |
42 | * |
43 | * @see session_cache_limiter() |
44 | * |
45 | */ |
46 | protected $cacheLimiter; |
47 | |
48 | /** |
49 | * |
50 | * The cache expiration time in minutes. |
51 | * |
52 | * @var int |
53 | * |
54 | * @see session_cache_expire() |
55 | * |
56 | */ |
57 | protected $cacheExpire; |
58 | |
59 | /** |
60 | * |
61 | * The current Unix timestamp. |
62 | * |
63 | * @var int |
64 | * |
65 | */ |
66 | protected $time; |
67 | |
68 | /** |
69 | * |
70 | * Constructor. |
71 | * |
72 | * @param string $cacheLimiter The cache limiter type. |
73 | * |
74 | * @param string $cacheExpire The cache expiration time in minutes. |
75 | * |
76 | * @throws RuntimeException when the ini settings are incorrect. |
77 | * |
78 | */ |
79 | public function __construct($cacheLimiter = 'nocache', $cacheExpire = 180) |
80 | { |
81 | ini_set('session.use_trans_sid', false); |
82 | ini_set('session.use_cookies', false); |
83 | ini_set('session.use_only_cookies', true); |
84 | ini_set('session.cache_limiter', ''); |
85 | |
86 | if (ini_get('session.use_trans_sid') != false) { |
87 | $message = "The .ini setting 'session.use_trans_sid' must be false."; |
88 | throw new RuntimeException($message); |
89 | } |
90 | |
91 | if (ini_get('session.use_cookies') != false) { |
92 | $message = "The .ini setting 'session.use_cookies' must be false."; |
93 | throw new RuntimeException($message); |
94 | } |
95 | |
96 | if (ini_get('session.use_only_cookies') != true) { |
97 | $message = "The .ini setting 'session.use_only_cookies' must be true."; |
98 | throw new RuntimeException($message); |
99 | } |
100 | |
101 | if (ini_get('session.cache_limiter') !== '') { |
102 | $message = "The .ini setting 'session.cache_limiter' must be an empty string."; |
103 | throw new RuntimeException($message); |
104 | } |
105 | |
106 | $this->cacheLimiter = $cacheLimiter; |
107 | $this->cacheExpire = (int) $cacheExpire; |
108 | } |
109 | |
110 | /** |
111 | * |
112 | * Sends the session headers in the Response. |
113 | * |
114 | * @param Request $request The HTTP request. |
115 | * |
116 | * @param Response $response The HTTP response. |
117 | * |
118 | * @param callable $next The next middleware in the queue. |
119 | * |
120 | * @return Response |
121 | * |
122 | */ |
123 | public function __invoke(Request $request, ?RequestHandlerInterface $next) |
124 | { |
125 | // retain the incoming session id |
126 | $oldId = ''; |
127 | $oldName = session_name(); |
128 | $cookies = $request->getCookieParams(); |
129 | if (! empty($cookies[$oldName])) { |
130 | $oldId = $cookies[$oldName]; |
131 | session_id($oldId); |
132 | } |
133 | |
134 | // invoke the next middleware |
135 | if (null !== $next) { |
136 | $response = $next->handle($request); |
137 | } else { |
138 | $response = (new ResponseFactory())->createResponse(); |
139 | } |
140 | |
141 | // record the current time |
142 | $this->time = time(); |
143 | |
144 | // is the session id still the same? |
145 | $newId = session_id(); |
146 | if ($newId !== $oldId) { |
147 | // one of the middlewares changed it; send the new one. |
148 | // capture any session name changes as well. |
149 | $response = $this->withNewSessionCookie($response, $newId); |
150 | } |
151 | |
152 | // if there is a session id, also send the cache limiters |
153 | if ($newId) { |
154 | $response = $this->withCacheLimiter($response); |
155 | } |
156 | |
157 | // done! |
158 | return $response; |
159 | } |
160 | |
161 | /** |
162 | * |
163 | * Adds a session cookie header to the Response. |
164 | * |
165 | * @param Response $response The HTTP response. |
166 | * |
167 | * @param string $sessionId The new session ID. |
168 | * |
169 | * @return Response |
170 | * |
171 | * @see https://github.com/php/php-src/blob/PHP-5.6.20/ext/session/session.c#L1337-L1408 |
172 | * |
173 | */ |
174 | protected function withNewSessionCookie(Response $response, $sessionId) |
175 | { |
176 | $cookie = urlencode(session_name()) . '=' . urlencode($sessionId); |
177 | |
178 | $params = session_get_cookie_params(); |
179 | |
180 | if ($params['lifetime']) { |
181 | $expires = $this->timestamp($params['lifetime']); |
182 | $cookie .= "; expires={$expires}; max-age={$params['lifetime']}"; |
183 | } |
184 | |
185 | if ($params['domain']) { |
186 | $cookie .= "; domain={$params['domain']}"; |
187 | } |
188 | |
189 | if ($params['path']) { |
190 | $cookie .= "; path={$params['path']}"; |
191 | } |
192 | |
193 | if ($params['secure']) { |
194 | $cookie .= '; secure'; |
195 | } |
196 | |
197 | if ($params['httponly']) { |
198 | $cookie .= '; httponly'; |
199 | } |
200 | |
201 | return $response->withAddedHeader('Set-Cookie', $cookie); |
202 | } |
203 | |
204 | /** |
205 | * |
206 | * Returns a cookie-formatted timestamp. |
207 | * |
208 | * @param int $adj Adjust the time by this many seconds before formatting. |
209 | * |
210 | * @return string |
211 | * |
212 | */ |
213 | protected function timestamp($adj = 0) |
214 | { |
215 | return gmdate('D, d M Y H:i:s T', $this->time + $adj); |
216 | } |
217 | |
218 | /** |
219 | * |
220 | * Returns a Response with added cache limiter headers. |
221 | * |
222 | * @param Response $response The HTTP response. |
223 | * |
224 | * @return Response |
225 | * |
226 | */ |
227 | protected function withCacheLimiter(Response $response) |
228 | { |
229 | switch ($this->cacheLimiter) { |
230 | case 'public': |
231 | return $this->cacheLimiterPublic($response); |
232 | case 'private_no_expire': |
233 | return $this->cacheLimiterPrivateNoExpire($response); |
234 | case 'private': |
235 | return $this->cacheLimiterPrivate($response); |
236 | case 'nocache': |
237 | return $this->cacheLimiterNocache($response); |
238 | default: |
239 | return $response; |
240 | } |
241 | } |
242 | |
243 | /** |
244 | * |
245 | * Returns a Response with 'public' cache limiter headers. |
246 | * |
247 | * @param Response $response The HTTP response. |
248 | * |
249 | * @return Response |
250 | * |
251 | * @see https://github.com/php/php-src/blob/PHP-5.6.20/ext/session/session.c#L1196-L1213 |
252 | * |
253 | */ |
254 | protected function cacheLimiterPublic(Response $response) |
255 | { |
256 | $maxAge = $this->cacheExpire * 60; |
257 | $expires = $this->timestamp($maxAge); |
258 | $cacheControl = "public, max-age={$maxAge}"; |
259 | $lastModified = $this->timestamp(); |
260 | |
261 | return $response |
262 | ->withAddedHeader('Expires', $expires) |
263 | ->withAddedHeader('Cache-Control', $cacheControl) |
264 | ->withAddedHeader('Last-Modified', $lastModified); |
265 | } |
266 | |
267 | /** |
268 | * |
269 | * Returns a Response with 'private_no_expire' cache limiter headers. |
270 | * |
271 | * @param Response $response The HTTP response. |
272 | * |
273 | * @return Response |
274 | * |
275 | * @see https://github.com/php/php-src/blob/PHP-5.6.20/ext/session/session.c#L1215-L1224 |
276 | * |
277 | */ |
278 | protected function cacheLimiterPrivateNoExpire(Response $response) |
279 | { |
280 | $maxAge = $this->cacheExpire * 60; |
281 | $cacheControl = "private, max-age={$maxAge}, pre-check={$maxAge}"; |
282 | $lastModified = $this->timestamp(); |
283 | |
284 | return $response |
285 | ->withAddedHeader('Cache-Control', $cacheControl) |
286 | ->withAddedHeader('Last-Modified', $lastModified); |
287 | } |
288 | |
289 | /** |
290 | * |
291 | * Returns a Response with 'private' cache limiter headers. |
292 | * |
293 | * @param Response $response The HTTP response. |
294 | * |
295 | * @return Response |
296 | * |
297 | * @see https://github.com/php/php-src/blob/PHP-5.6.20/ext/session/session.c#L1226-L1231 |
298 | * |
299 | */ |
300 | protected function cacheLimiterPrivate(Response $response) |
301 | { |
302 | if (0 == count($response->getHeader('Expires'))) { |
303 | $response = $response->withAddedHeader('Expires', self::EXPIRED); |
304 | } |
305 | return $this->cacheLimiterPrivateNoExpire($response); |
306 | } |
307 | |
308 | /** |
309 | * |
310 | * Returns a Response with 'nocache' cache limiter headers. |
311 | * |
312 | * @param Response $response The HTTP response. |
313 | * |
314 | * @return Response |
315 | * |
316 | * @see https://github.com/php/php-src/blob/PHP-5.6.20/ext/session/session.c#L1233-L1243 |
317 | * |
318 | */ |
319 | protected function cacheLimiterNocache(Response $response) |
320 | { |
321 | if (0 == count($response->getHeader('Expires'))) { |
322 | $response = $response->withAddedHeader('Expires', self::EXPIRED); |
323 | } |
324 | return $response |
325 | ->withAddedHeader( |
326 | 'Cache-Control', |
327 | 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0' |
328 | ) |
329 | ->withAddedHeader('Pragma', 'no-cache'); |
330 | } |
331 | } |