├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── CacheSessionPersistence.php ├── CacheSessionPersistenceFactory.php ├── ConfigProvider.php └── Exception ├── ExceptionInterface.php ├── InvalidArgumentException.php └── MissingDependencyException.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 1.3.2 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 1.3.1 - 2019-06-24 28 | 29 | ### Added 30 | 31 | - [#8](https://github.com/zendframework/zend-expressive-session-cache/pull/8) adds support for PHP 7.3. 32 | 33 | ### Changed 34 | 35 | - Nothing. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - Nothing. 44 | 45 | ### Fixed 46 | 47 | - Nothing. 48 | 49 | ## 1.3.0 - 2019-01-22 50 | 51 | ### Added 52 | 53 | - [#7](https://github.com/zendframework/zend-expressive-session-cache/pull/7) adds the ability to set the session cookie domain, secure, and 54 | httponly options. Each may be passed to the `CacheSessionPersistence` 55 | constructor, or as options consumed by its factory. See the documentation for 56 | full details. 57 | 58 | ### Changed 59 | 60 | - Nothing. 61 | 62 | ### Deprecated 63 | 64 | - Nothing. 65 | 66 | ### Removed 67 | 68 | - Nothing. 69 | 70 | ### Fixed 71 | 72 | - Nothing. 73 | 74 | ## 1.2.0 - 2018-10-31 75 | 76 | ### Added 77 | 78 | - [#5](https://github.com/zendframework/zend-expressive-session-cache/pull/5) adds support for the new `SessionCookiePersistenceInterface` added 79 | in zend-expressive-session 1.2.0. Specifically, `CacheSessionPersistence` now 80 | queries the session instance `getSessionLifetime()` method to determine 81 | whether or not to send an `Expires` directive with the session cookie. 82 | 83 | ### Changed 84 | 85 | - Nothing. 86 | 87 | ### Deprecated 88 | 89 | - Nothing. 90 | 91 | ### Removed 92 | 93 | - Nothing. 94 | 95 | ### Fixed 96 | 97 | - Nothing. 98 | 99 | ## 1.1.1 - 2018-10-26 100 | 101 | ### Added 102 | 103 | - Nothing. 104 | 105 | ### Changed 106 | 107 | - [#4](https://github.com/zendframework/zend-expressive-session-cache/pull/4) modifies the behavior when setting a persistent cookie. Previously, 108 | it would set a Max-Age directive on the cookie; however, this is not supported 109 | in all browsers or SAPIs. As such, it now creates an Expires directive, which 110 | will have essentially the same effect for users. 111 | 112 | ### Deprecated 113 | 114 | - Nothing. 115 | 116 | ### Removed 117 | 118 | - Nothing. 119 | 120 | ### Fixed 121 | 122 | - Nothing. 123 | 124 | ## 1.1.0 - 2018-10-25 125 | 126 | ### Added 127 | 128 | - [#3](https://github.com/zendframework/zend-expressive-session-cache/pull/3) adds a new constructor argument, `bool $persistent = false`. When 129 | this is toggled to `true`, a `Max-Age` directive will be added with a value 130 | equivalent to the `$cacheExpire` value. You can configure this value using the 131 | `zend-expressive-session-cache.persistent` configuration key. 132 | 133 | ### Changed 134 | 135 | - Nothing. 136 | 137 | ### Deprecated 138 | 139 | - Nothing. 140 | 141 | ### Removed 142 | 143 | - Nothing. 144 | 145 | ### Fixed 146 | 147 | - Nothing. 148 | 149 | ## 1.0.0 - 2018-10-09 150 | 151 | ### Added 152 | 153 | - Everything. 154 | 155 | ### Changed 156 | 157 | - Nothing. 158 | 159 | ### Deprecated 160 | 161 | - Nothing. 162 | 163 | ### Removed 164 | 165 | - Nothing. 166 | 167 | ### Fixed 168 | 169 | - Nothing. 170 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zend-expressive-session-cache 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [mezzio/mezzio-session-cache](https://github.com/mezzio/mezzio-session-cache). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-expressive-session-cache.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-expressive-session-cache) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-expressive-session-cache/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-expressive-session-cache?branch=master) 9 | 10 | This library provides a [PSR-6](https://www.php-fig.org/psr/psr-6/) session 11 | persistence adapter for [zend-expressive-session](https://docs.zendframework.com/zend-expressive-session/). 12 | 13 | ## Installation 14 | 15 | Run the following to install this library: 16 | 17 | ```bash 18 | $ composer require zendframework/zend-expressive-session-cache 19 | ``` 20 | 21 | ## Documentation 22 | 23 | Browse the documentation online at https://docs.zendframework.com/zend-expressive-session-cache/ 24 | 25 | ## Support 26 | 27 | * [Issues](https://github.com/zendframework/zend-expressive-session-cache/issues/) 28 | * [Chat](https://zendframework-slack.herokuapp.com/) 29 | * [Forum](https://discourse.zendframework.com/) 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-expressive-session-cache", 3 | "description": "PSR-6 session persistence adapter for zend-expressive-session.", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "expressive", 7 | "zf", 8 | "zendframework", 9 | "cache", 10 | "psr-6", 11 | "session" 12 | ], 13 | "support": { 14 | "docs": "https://docs.zendframework.com/zend-expressive-session-cache/", 15 | "issues": "https://github.com/zendframework/zend-expressive-session-cache/issues", 16 | "source": "https://github.com/zendframework/zend-expressive-session-cache", 17 | "rss": "https://github.com/zendframework/zend-expressive-session-cache/releases.atom", 18 | "chat": "https://zendframework-slack.herokuapp.com", 19 | "forum": "https://discourse.zendframework.com/c/questions/components" 20 | }, 21 | "require": { 22 | "php": "^7.1", 23 | "dflydev/fig-cookies": "^1.0.2 || ^2.0", 24 | "psr/cache": "^1.0", 25 | "psr/container": "^1.0", 26 | "zendframework/zend-expressive-session": "^1.2" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^7.1.1", 30 | "zendframework/zend-coding-standard": "~1.0.0", 31 | "zendframework/zend-diactoros": "^2.0" 32 | }, 33 | "suggest": { 34 | "psr/cache-implementation": "This package requires a PSR-6 CacheItemPoolInterface implementation." 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Zend\\Expressive\\Session\\Cache\\": "src/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "ZendTest\\Expressive\\Session\\Cache\\": "test/" 44 | } 45 | }, 46 | "config": { 47 | "sort-packages": true 48 | }, 49 | "extra": { 50 | "branch-alias": { 51 | "dev-master": "1.3.x-dev", 52 | "dev-develop": "1.4.x-dev" 53 | }, 54 | "zf": { 55 | "config-provider": "Zend\\Expressive\\Session\\Cache\\ConfigProvider" 56 | } 57 | }, 58 | "scripts": { 59 | "check": [ 60 | "@cs-check", 61 | "@test" 62 | ], 63 | "cs-check": "phpcs", 64 | "cs-fix": "phpcbf", 65 | "test": "phpunit --colors=always", 66 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/CacheSessionPersistence.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 139 | 140 | if (empty($cookieName)) { 141 | throw new Exception\InvalidArgumentException('Session cookie name must not be empty'); 142 | } 143 | $this->cookieName = $cookieName; 144 | 145 | $this->cookieDomain = $cookieDomain; 146 | 147 | $this->cookiePath = $cookiePath; 148 | 149 | $this->cookieSecure = $cookieSecure; 150 | 151 | $this->cookieHttpOnly = $cookieHttpOnly; 152 | 153 | $this->cacheLimiter = in_array($cacheLimiter, self::SUPPORTED_CACHE_LIMITERS, true) 154 | ? $cacheLimiter 155 | : 'nocache'; 156 | 157 | $this->cacheExpire = $cacheExpire; 158 | 159 | $this->lastModified = $lastModified 160 | ? gmdate(self::HTTP_DATE_FORMAT, $lastModified) 161 | : $this->determineLastModifiedValue(); 162 | 163 | $this->persistent = $persistent; 164 | } 165 | 166 | public function initializeSessionFromRequest(ServerRequestInterface $request) : SessionInterface 167 | { 168 | $id = $this->getCookieFromRequest($request); 169 | $sessionData = $id ? $this->getSessionDataFromCache($id) : []; 170 | return new Session($sessionData, $id); 171 | } 172 | 173 | public function persistSession(SessionInterface $session, ResponseInterface $response) : ResponseInterface 174 | { 175 | $id = $session->getId(); 176 | 177 | // New session? No data? Nothing to do. 178 | if ('' === $id 179 | && ([] === $session->toArray() || ! $session->hasChanged()) 180 | ) { 181 | return $response; 182 | } 183 | 184 | // Regenerate the session if: 185 | // - we have no session identifier 186 | // - the session is marked as regenerated 187 | // - the session has changed (data is different) 188 | if ('' === $id || $session->isRegenerated() || $session->hasChanged()) { 189 | $id = $this->regenerateSession($id); 190 | } 191 | 192 | $this->persistSessionDataToCache($id, $session->toArray()); 193 | 194 | $sessionCookie = SetCookie::create($this->cookieName) 195 | ->withValue($id) 196 | ->withDomain($this->cookieDomain) 197 | ->withPath($this->cookiePath) 198 | ->withSecure($this->cookieSecure) 199 | ->withHttpOnly($this->cookieHttpOnly); 200 | 201 | $persistenceDuration = $this->getPersistenceDuration($session); 202 | if ($persistenceDuration) { 203 | $sessionCookie = $sessionCookie->withExpires( 204 | (new DateTimeImmutable())->add(new DateInterval(sprintf('PT%dS', $persistenceDuration))) 205 | ); 206 | } 207 | 208 | $response = FigResponseCookies::set($response, $sessionCookie); 209 | 210 | if ($this->responseAlreadyHasCacheHeaders($response)) { 211 | return $response; 212 | } 213 | 214 | foreach ($this->generateCacheHeaders() as $name => $value) { 215 | if (false !== $value) { 216 | $response = $response->withHeader($name, $value); 217 | } 218 | } 219 | 220 | return $response; 221 | } 222 | 223 | /** 224 | * Regenerates the session. 225 | * 226 | * If the cache has an entry corresponding to `$id`, this deletes it. 227 | * 228 | * Regardless, it generates and returns a new session identifier. 229 | */ 230 | private function regenerateSession(string $id) : string 231 | { 232 | if ('' !== $id && $this->cache->hasItem($id)) { 233 | $this->cache->deleteItem($id); 234 | } 235 | return $this->generateSessionId(); 236 | } 237 | 238 | /** 239 | * Generate a session identifier. 240 | */ 241 | private function generateSessionId() : string 242 | { 243 | return bin2hex(random_bytes(16)); 244 | } 245 | 246 | /** 247 | * Generate cache http headers for this instance's session cache_limiter and 248 | * cache_expire values 249 | */ 250 | private function generateCacheHeaders() : array 251 | { 252 | // cache_limiter: 'nocache' 253 | if ('nocache' === $this->cacheLimiter) { 254 | return [ 255 | 'Expires' => self::CACHE_PAST_DATE, 256 | 'Cache-Control' => 'no-store, no-cache, must-revalidate', 257 | 'Pragma' => 'no-cache', 258 | ]; 259 | } 260 | 261 | // cache_limiter: 'public' 262 | if ('public' === $this->cacheLimiter) { 263 | return [ 264 | 'Expires' => gmdate(self::HTTP_DATE_FORMAT, time() + $this->cacheExpire), 265 | 'Cache-Control' => sprintf('public, max-age=%d', $this->cacheExpire), 266 | 'Last-Modified' => $this->lastModified, 267 | ]; 268 | } 269 | 270 | // cache_limiter: 'private' 271 | if ('private' === $this->cacheLimiter) { 272 | return [ 273 | 'Expires' => self::CACHE_PAST_DATE, 274 | 'Cache-Control' => sprintf('private, max-age=%d', $this->cacheExpire), 275 | 'Last-Modified' => $this->lastModified, 276 | ]; 277 | } 278 | 279 | // last possible case, cache_limiter = 'private_no_expire' 280 | return [ 281 | 'Cache-Control' => sprintf('private, max-age=%d', $this->cacheExpire), 282 | 'Last-Modified' => $this->lastModified, 283 | ]; 284 | } 285 | 286 | /** 287 | * Return the Last-Modified header line based on the request's script file 288 | * modified time. If no script file could be derived from the request we use 289 | * the file modification time of the current working directory as a fallback. 290 | * 291 | * @return string 292 | */ 293 | private function determineLastModifiedValue() : string 294 | { 295 | $cwd = getcwd(); 296 | foreach (['public/index.php', 'index.php'] as $filename) { 297 | $path = sprintf('%s/%s', $cwd, $filename); 298 | if (! file_exists($path)) { 299 | continue; 300 | } 301 | 302 | return gmdate(self::HTTP_DATE_FORMAT, filemtime($path)); 303 | } 304 | 305 | return gmdate(self::HTTP_DATE_FORMAT, filemtime($cwd)); 306 | } 307 | 308 | /** 309 | * Retrieve the session cookie value. 310 | * 311 | * Cookie headers may or may not be present, based on SAPI. For instance, 312 | * under Swoole, they are omitted, but the cookie parameters are present. 313 | * As such, this method uses FigRequestCookies to retrieve the cookie value 314 | * only if the Cookie header is present. Otherwise, it falls back to the 315 | * request cookie parameters. 316 | * 317 | * In each case, if the value is not found, it falls back to generating a 318 | * new session identifier. 319 | */ 320 | private function getCookieFromRequest(ServerRequestInterface $request) : string 321 | { 322 | if ('' !== $request->getHeaderLine('Cookie')) { 323 | return FigRequestCookies::get($request, $this->cookieName)->getValue() ?? ''; 324 | } 325 | 326 | return $request->getCookieParams()[$this->cookieName] ?? ''; 327 | } 328 | 329 | private function getSessionDataFromCache(string $id) : array 330 | { 331 | $item = $this->cache->getItem($id); 332 | if (! $item->isHit()) { 333 | return []; 334 | } 335 | return $item->get() ?: []; 336 | } 337 | 338 | private function persistSessionDataToCache(string $id, array $data) : void 339 | { 340 | $item = $this->cache->getItem($id); 341 | $item->set($data); 342 | $item->expiresAfter($this->cacheExpire); 343 | $this->cache->save($item); 344 | } 345 | 346 | /** 347 | * Check if the response already carries cache headers 348 | */ 349 | private function responseAlreadyHasCacheHeaders(ResponseInterface $response) : bool 350 | { 351 | return ( 352 | $response->hasHeader('Expires') 353 | || $response->hasHeader('Last-Modified') 354 | || $response->hasHeader('Cache-Control') 355 | || $response->hasHeader('Pragma') 356 | ); 357 | } 358 | 359 | private function getPersistenceDuration(SessionInterface $session) : int 360 | { 361 | $duration = $this->persistent ? $this->cacheExpire : 0; 362 | if ($session instanceof SessionCookiePersistenceInterface 363 | && $session->has(SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY) 364 | ) { 365 | $duration = $session->getSessionLifetime(); 366 | } 367 | return $duration < 0 ? 0 : $duration; 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /src/CacheSessionPersistenceFactory.php: -------------------------------------------------------------------------------- 1 | has('config') ? $container->get('config') : []; 20 | $config = $config['zend-expressive-session-cache'] ?? []; 21 | 22 | $cacheService = $config['cache_item_pool_service'] ?? CacheItemPoolInterface::class; 23 | 24 | if (! $container->has($cacheService)) { 25 | throw Exception\MissingDependencyException::forService($cacheService); 26 | } 27 | 28 | $cookieName = $config['cookie_name'] ?? 'PHPSESSION'; 29 | $cookieDomain = $config['cookie_domain'] ?? null; 30 | $cookiePath = $config['cookie_path'] ?? '/'; 31 | $cookieSecure = $config['cookie_secure'] ?? false; 32 | $cookieHttpOnly = $config['cookie_http_only'] ?? false; 33 | $cacheLimiter = $config['cache_limiter'] ?? 'nocache'; 34 | $cacheExpire = $config['cache_expire'] ?? 10800; 35 | $lastModified = $config['last_modified'] ?? null; 36 | $persistent = $config['persistent'] ?? false; 37 | 38 | return new CacheSessionPersistence( 39 | $container->get($cacheService), 40 | $cookieName, 41 | $cookiePath, 42 | $cacheLimiter, 43 | $cacheExpire, 44 | $lastModified, 45 | $persistent, 46 | $cookieDomain, 47 | $cookieSecure, 48 | $cookieHttpOnly 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | $this->getDependencies(), 18 | ]; 19 | } 20 | 21 | public function getDependencies() : array 22 | { 23 | return [ 24 | 'factories' => [ 25 | CacheSessionPersistence::class => CacheSessionPersistenceFactory::class, 26 | ], 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 |