Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 84
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
SessionHeadersHandler
0.00% covered (danger)
0.00%
0 / 84
0.00% covered (danger)
0.00%
0 / 9
870
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 __invoke
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 withNewSessionCookie
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 timestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 withCacheLimiter
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 cacheLimiterPublic
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 cacheLimiterPrivateNoExpire
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 cacheLimiterPrivate
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 cacheLimiterNocache
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BO\Slim\Middleware;
4
5use Psr\Http\Message\ResponseInterface as Response;
6use Psr\Http\Message\ServerRequestInterface as Request;
7use Psr\Http\Server\RequestHandlerInterface;
8use RuntimeException;
9use 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 */
30class 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     * @param RequestHandlerInterface|null $next The next middleware in the queue.
116     *
117     * @return Response
118     *
119     */
120    public function __invoke(Request $request, ?RequestHandlerInterface $next)
121    {
122        // retain the incoming session id
123        $oldId = '';
124        $oldName = session_name();
125        $cookies = $request->getCookieParams();
126        if (! empty($cookies[$oldName])) {
127            $oldId = $cookies[$oldName];
128            session_id($oldId);
129        }
130
131        // invoke the next middleware
132        if (null !== $next) {
133            $response = $next->handle($request);
134        } else {
135            $response = (new ResponseFactory())->createResponse();
136        }
137
138        // record the current time
139        $this->time = time();
140
141        // is the session id still the same?
142        $newId = session_id();
143        if ($newId !== $oldId) {
144            // one of the middlewares changed it; send the new one.
145            // capture any session name changes as well.
146            $response = $this->withNewSessionCookie($response, $newId);
147        }
148
149        // if there is a session id, also send the cache limiters
150        if ($newId) {
151            $response = $this->withCacheLimiter($response);
152        }
153
154        // done!
155        return $response;
156    }
157
158    /**
159     *
160     * Adds a session cookie header to the Response.
161     *
162     * @param Response $response The HTTP response.
163     *
164     * @param string $sessionId The new session ID.
165     *
166     * @return Response
167     *
168     * @see https://github.com/php/php-src/blob/PHP-5.6.20/ext/session/session.c#L1337-L1408
169     *
170     */
171    protected function withNewSessionCookie(Response $response, $sessionId)
172    {
173        $cookie = urlencode(session_name()) . '=' . urlencode($sessionId);
174
175        $params = session_get_cookie_params();
176
177        if ($params['lifetime']) {
178            $expires = $this->timestamp($params['lifetime']);
179            $cookie .= "; expires={$expires}; max-age={$params['lifetime']}";
180        }
181
182        if ($params['domain']) {
183            $cookie .= "; domain={$params['domain']}";
184        }
185
186        if ($params['path']) {
187            $cookie .= "; path={$params['path']}";
188        }
189
190        if ($params['secure']) {
191            $cookie .= '; secure';
192        }
193
194        if ($params['httponly']) {
195            $cookie .= '; httponly';
196        }
197
198        return $response->withAddedHeader('Set-Cookie', $cookie);
199    }
200
201    /**
202     *
203     * Returns a cookie-formatted timestamp.
204     *
205     * @param int $adj Adjust the time by this many seconds before formatting.
206     *
207     * @return string
208     *
209     */
210    protected function timestamp($adj = 0)
211    {
212        return gmdate('D, d M Y H:i:s T', $this->time + $adj);
213    }
214
215    /**
216     *
217     * Returns a Response with added cache limiter headers.
218     *
219     * @param Response $response The HTTP response.
220     *
221     * @return Response
222     *
223     */
224    protected function withCacheLimiter(Response $response)
225    {
226        switch ($this->cacheLimiter) {
227            case 'public':
228                return $this->cacheLimiterPublic($response);
229            case 'private_no_expire':
230                return $this->cacheLimiterPrivateNoExpire($response);
231            case 'private':
232                return $this->cacheLimiterPrivate($response);
233            case 'nocache':
234                return $this->cacheLimiterNocache($response);
235            default:
236                return $response;
237        }
238    }
239
240    /**
241     *
242     * Returns a Response with 'public' cache limiter headers.
243     *
244     * @param Response $response The HTTP response.
245     *
246     * @return Response
247     *
248     * @see https://github.com/php/php-src/blob/PHP-5.6.20/ext/session/session.c#L1196-L1213
249     *
250     */
251    protected function cacheLimiterPublic(Response $response)
252    {
253        $maxAge = $this->cacheExpire * 60;
254        $expires = $this->timestamp($maxAge);
255        $cacheControl = "public, max-age={$maxAge}";
256        $lastModified = $this->timestamp();
257
258        return $response
259            ->withAddedHeader('Expires', $expires)
260            ->withAddedHeader('Cache-Control', $cacheControl)
261            ->withAddedHeader('Last-Modified', $lastModified);
262    }
263
264    /**
265     *
266     * Returns a Response with 'private_no_expire' cache limiter headers.
267     *
268     * @param Response $response The HTTP response.
269     *
270     * @return Response
271     *
272     * @see https://github.com/php/php-src/blob/PHP-5.6.20/ext/session/session.c#L1215-L1224
273     *
274     */
275    protected function cacheLimiterPrivateNoExpire(Response $response)
276    {
277        $maxAge = $this->cacheExpire * 60;
278        $cacheControl = "private, max-age={$maxAge}, pre-check={$maxAge}";
279        $lastModified = $this->timestamp();
280
281        return $response
282            ->withAddedHeader('Cache-Control', $cacheControl)
283            ->withAddedHeader('Last-Modified', $lastModified);
284    }
285
286    /**
287     *
288     * Returns a Response with 'private' cache limiter headers.
289     *
290     * @param Response $response The HTTP response.
291     *
292     * @return Response
293     *
294     * @see https://github.com/php/php-src/blob/PHP-5.6.20/ext/session/session.c#L1226-L1231
295     *
296     */
297    protected function cacheLimiterPrivate(Response $response)
298    {
299        if (0 == count($response->getHeader('Expires'))) {
300            $response = $response->withAddedHeader('Expires', self::EXPIRED);
301        }
302        return $this->cacheLimiterPrivateNoExpire($response);
303    }
304
305    /**
306     *
307     * Returns a Response with 'nocache' cache limiter headers.
308     *
309     * @param Response $response The HTTP response.
310     *
311     * @return Response
312     *
313     * @see https://github.com/php/php-src/blob/PHP-5.6.20/ext/session/session.c#L1233-L1243
314     *
315     */
316    protected function cacheLimiterNocache(Response $response)
317    {
318        if (0 == count($response->getHeader('Expires'))) {
319            $response = $response->withAddedHeader('Expires', self::EXPIRED);
320        }
321        return $response
322            ->withAddedHeader(
323                'Cache-Control',
324                'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'
325            )
326            ->withAddedHeader('Pragma', 'no-cache');
327    }
328}