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 | } |