├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── TODO.md ├── composer.json └── src ├── ConfigProvider.php ├── Exception ├── ExceptionInterface.php ├── InvalidHopsValueException.php ├── InvalidSessionSegmentDataException.php ├── NotInitializableException.php └── SessionSegmentConflictException.php ├── InitializePersistenceIdInterface.php ├── InitializeSessionIdInterface.php ├── LazySession.php ├── Session.php ├── SessionCookiePersistenceInterface.php ├── SessionIdentifierAwareInterface.php ├── SessionInterface.php ├── SessionMiddleware.php ├── SessionMiddlewareFactory.php └── SessionPersistenceInterface.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.1 - 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.0 - 2019-10-16 28 | 29 | ### Added 30 | 31 | - [#38](https://github.com/zendframework/zend-expressive-session/pull/38) adds `InitializeSessionIdInterface` and `InitializePersistenceIdInterface`. These add `initializeId()` methods to session and persistence, allowing developers to access new or regenerated session IDs before the session is persisted. 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.2.1 - 2019-03-05 50 | 51 | ### Added 52 | 53 | - [#33](https://github.com/zendframework/zend-expressive-session/pull/33) adds support for PHP 7.3. 54 | 55 | ### Changed 56 | 57 | - [#34](https://github.com/zendframework/zend-expressive-session/pull/34) provides several performance optimizations in `Zend\Expressives\Session\LazySession`. 58 | 59 | ### Deprecated 60 | 61 | - Nothing. 62 | 63 | ### Removed 64 | 65 | - Nothing. 66 | 67 | ### Fixed 68 | 69 | - Nothing. 70 | 71 | ## 1.2.0 - 2018-10-30 72 | 73 | ### Added 74 | 75 | - [#28](https://github.com/zendframework/zend-expressive-session/pull/28) adds a new interface, `SessionCookiePersistenceInterface`, defining: 76 | - the constant `SESSION_LIFETIME_KEY` 77 | - the method `persistSessionFor(int $duration) : void`, for developers to hint 78 | to the persistence engine how long a session should last 79 | - the method `getSessionLifetime() : int`, for persistence engines to 80 | determine if a specific session duration was requested 81 | 82 | ### Changed 83 | 84 | - [#28](https://github.com/zendframework/zend-expressive-session/pull/28) updates both `Session` and `LazySession` to implement the new 85 | `SessionCookiePersistenceInterface. If a `SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY` 86 | is present in the initial session data provided to a `Session` instance, this 87 | value will be used to indicate the requested session duration; otherwise, zero 88 | is used, indicating the session should end when the browser is closed. 89 | 90 | ### Deprecated 91 | 92 | - Nothing. 93 | 94 | ### Removed 95 | 96 | - Nothing. 97 | 98 | ### Fixed 99 | 100 | - Nothing. 101 | 102 | ## 1.1.0 - 2018-09-12 103 | 104 | ### Added 105 | 106 | - [#27](https://github.com/zendframework/zend-expressive-session/pull/27) adds a new interface, `Zend\Expressive\Session\SessionIdentifierAwareInterface`. 107 | `SessionInterface` implementations should also implement this interface, and 108 | persistence implementations should only create and consume session 109 | implementations that implement it. The interface defines a single method, 110 | `getId()`, representing the identifier of a discovered session. This allows 111 | the identifier to be associated with its session data, ensuring that when 112 | concurrent requests are made, persistence operates on the correct identifier. 113 | 114 | ### Changed 115 | 116 | - Nothing. 117 | 118 | ### Deprecated 119 | 120 | - Nothing. 121 | 122 | ### Removed 123 | 124 | - Nothing. 125 | 126 | ### Fixed 127 | 128 | - Nothing. 129 | 130 | ## 1.0.0 - 2018-03-15 131 | 132 | ### Added 133 | 134 | - [#18](https://github.com/zendframework/zend-expressive-session/pull/18) adds 135 | support for PSR-15 middleware. 136 | 137 | ### Changed 138 | 139 | - Nothing. 140 | 141 | ### Deprecated 142 | 143 | - Nothing. 144 | 145 | ### Removed 146 | 147 | - [#14](https://github.com/zendframework/zend-expressive-session/pull/14) and 148 | [#18](https://github.com/zendframework/zend-expressive-session/pull/18) remove 149 | support for http-interop/http-middleware and http-interop/http-server-middleware. 150 | 151 | - [#5](https://github.com/zendframework/zend-expressive-session/pull/5) removes 152 | the method `LazySession::segment()`. This method was a remnant from a previous 153 | refactor, and not intended for the final API. Considering that `Session` does 154 | not implement the method, calling it would lead to a fatal error anyways. 155 | 156 | ### Fixed 157 | 158 | - Nothing. 159 | 160 | ## 0.1.0 - 2017-10-10 161 | 162 | Initial release. 163 | 164 | ### Added 165 | 166 | - Everything. 167 | 168 | ### Changed 169 | 170 | - Nothing. 171 | 172 | ### Deprecated 173 | 174 | - Nothing. 175 | 176 | ### Removed 177 | 178 | - Nothing. 179 | 180 | ### Fixed 181 | 182 | - Nothing. 183 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, 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 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [mezzio/mezzio-session](https://github.com/mezzio/mezzio-session). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-expressive-session.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-expressive-session) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-expressive-session/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-expressive-session?branch=master) 9 | 10 | This library provides session handling middleware for PSR-7 applications, using 11 | an adapter-based approach that will allow usage of ext-session, JWT, or other 12 | approaches. 13 | 14 | ## Installation 15 | 16 | Run the following to install this library: 17 | 18 | ```bash 19 | $ composer require zendframework/zend-expressive-session 20 | ``` 21 | 22 | However, the package is not immediately useful unless you have a persistence 23 | adapter. If you are okay with using ext-session, you can install the following 24 | package as well: 25 | 26 | ```bash 27 | $ composer require zendframework/zend-expressive-session-ext 28 | ``` 29 | 30 | ## Documentation 31 | 32 | Documentation is [in the doc tree](docs/book/), and can be compiled using [mkdocs](http://www.mkdocs.org): 33 | 34 | ```bash 35 | $ mkdocs build 36 | ``` 37 | 38 | You may also [browse the documentation online](https://docs.zendframework.com/zend-expressive-session/). 39 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## Persistence implementations 4 | 5 | - [ ] Externalize `PhpSessionPersistence` to a separate package that requires 6 | `ext-session`. 7 | 8 | ## Segments 9 | 10 | Evaluate the `SegmentInterface` and `Segment` class and their responsibilities 11 | to see if they should be included in the base session package. 12 | 13 | Marco argues that segments could be accomplished as _additional session 14 | containers_, each with their own cookie. This obviously will not work with 15 | ext-session, however. 16 | 17 | The main reason for session segments/namespaces is to allow grouping contextual 18 | data, and preventing conflicts. This can still be achieved by having nested 19 | arrays; the access is just not as pretty: 20 | 21 | ```php 22 | // With segments: 23 | $username = $session->segment('authentication')->get('username'); 24 | // versus without: 25 | $username = $session->get('authentication')['username']); 26 | ``` 27 | 28 | Overall, _separate sessions_ provides a cleaner approach to this; it's just not 29 | something that can be done with ext-session. The interfaces as modeled, however, 30 | would allow for a non-ext-session approach to sessions (e.g., using caching 31 | storage such as redis, memcached, or couchdb) that could accommodate the 32 | approach. 33 | 34 | - [x] Remove segment implementation 35 | 36 | ### Flash messages 37 | 38 | Marco suggests that flash messages are something to build _on top of_ a 39 | session package. A "Flash" class (or similar) would receive a session 40 | container to its constructor, and manipulate it in order to extract, 41 | persist, or expire messages. That instance could then be persisted as a 42 | session attribute. 43 | 44 | The main issue I see with this approach is that segregating flash messages by 45 | context is harder. This can be accomplished by having different instances 46 | operating on different session variables, however. Doing so leads to a need for 47 | different request attributes, though, which might become confusing in large 48 | applications — though likely no more confusing than learning which session 49 | "segments" are used in which contexts of the application. 50 | 51 | - [x] Create a separate package for flash messages 52 | - [x] Introduce an interface just for flash message access and manipulation. 53 | - [x] Rename `persistFlash()` to something more appropriate. 54 | - [x] Create middleware for creating the `Flash` instance and propagating it 55 | into the request delegated by the middleware. 56 | - [x] Externalize the flash message support to its own package 57 | 58 | ### CSRF protection 59 | 60 | Marco has a middleware-based implementation in Ocramius/PSR7Csrf that 61 | essentially pulls from a specific `Session` variable, and acts as a guard for 62 | non-GET|HEAD|OPTIONS method calls, returning a specific response if validation 63 | of the CSRF token fails. Essentially, the argument is that this can be built _on 64 | top of_ a session package. 65 | 66 | Ocramius/PSR7Csrf could potentially be adapted later to work with 67 | zend-expressive-session, meaning we wouldn't need to add any such capabilities 68 | to our own package, or even write our own package, for handling CSRF. 69 | 70 | - [x] Extract an interface for generating, validating CSRF values 71 | - [x] Create one or more implementations of the interface 72 | - [x] Create middleware for generating and injecting the CSRF guard into the request 73 | - [x] Externalize the CSRF support to its own package 74 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-expressive-session", 3 | "description": "Session container and middleware for PSR-7 applications", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "expressive", 7 | "middleware", 8 | "psr-7", 9 | "session", 10 | "zf", 11 | "zendframework", 12 | "zend-expressive" 13 | ], 14 | "support": { 15 | "docs": "https://docs.zendframework.com/zend-expressive-session/", 16 | "issues": "https://github.com/zendframework/zend-expressive-session/issues", 17 | "source": "https://github.com/zendframework/zend-expressive-session", 18 | "rss": "https://github.com/zendframework/zend-expressive-session/releases.atom", 19 | "slack": "https://zendframework-slack.herokuapp.com", 20 | "forum": "https://discourse.zendframework.com/c/questions/expressive" 21 | }, 22 | "require": { 23 | "php": "^7.1", 24 | "psr/container": "^1.0", 25 | "psr/http-server-middleware": "^1.0" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^6.5.5", 29 | "zendframework/zend-coding-standard": "~1.0.0" 30 | }, 31 | "conflict": { 32 | "phpspec/prophecy": "<1.7.2" 33 | }, 34 | "suggest": { 35 | "zendframework/zend-expressive-csrf": "^1.0 || ^1.0-dev for CSRF protection capabilities", 36 | "zendframework/zend-expressive-flash": "^1.0 || ^1.0-dev for flash message capabilities", 37 | "zendframework/zend-expressive-session-ext": "^1.0 || ^1.0-dev for an ext-session persistence adapter" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Zend\\Expressive\\Session\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "ZendTest\\Expressive\\Session\\": "test/" 47 | } 48 | }, 49 | "config": { 50 | "sort-packages": true 51 | }, 52 | "extra": { 53 | "branch-alias": { 54 | "dev-master": "1.3.x-dev", 55 | "dev-develop": "1.4.x-dev" 56 | }, 57 | "zf": { 58 | "config-provider": "Zend\\Expressive\\Session\\ConfigProvider" 59 | } 60 | }, 61 | "scripts": { 62 | "check": [ 63 | "@cs-check", 64 | "@test" 65 | ], 66 | "cs-check": "phpcs", 67 | "cs-fix": "phpcbf", 68 | "test": "phpunit --colors=always", 69 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | $this->getDependencies(), 18 | ]; 19 | } 20 | 21 | public function getDependencies() : array 22 | { 23 | return [ 24 | 'factories' => [ 25 | SessionMiddleware::class => SessionMiddlewareFactory::class, 26 | ], 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | initializeSessionFromRequest() 41 | * 42 | * @var ServerRequestInterface 43 | */ 44 | private $request; 45 | 46 | public function __construct(SessionPersistenceInterface $persistence, ServerRequestInterface $request) 47 | { 48 | $this->persistence = $persistence; 49 | $this->request = $request; 50 | } 51 | 52 | public function regenerate() : SessionInterface 53 | { 54 | $this->proxiedSession = $this->getProxiedSession()->regenerate(); 55 | return $this; 56 | } 57 | 58 | public function isRegenerated() : bool 59 | { 60 | if (! $this->proxiedSession) { 61 | return false; 62 | } 63 | 64 | return $this->proxiedSession->isRegenerated(); 65 | } 66 | 67 | public function toArray() : array 68 | { 69 | return $this->getProxiedSession()->toArray(); 70 | } 71 | 72 | public function get(string $name, $default = null) 73 | { 74 | return $this->getProxiedSession()->get($name, $default); 75 | } 76 | 77 | public function has(string $name) : bool 78 | { 79 | return $this->getProxiedSession()->has($name); 80 | } 81 | 82 | public function set(string $name, $value) : void 83 | { 84 | $this->getProxiedSession()->set($name, $value); 85 | } 86 | 87 | public function unset(string $name) : void 88 | { 89 | $this->getProxiedSession()->unset($name); 90 | } 91 | 92 | public function clear() : void 93 | { 94 | $this->getProxiedSession()->clear(); 95 | } 96 | 97 | public function hasChanged() : bool 98 | { 99 | if (! $this->proxiedSession) { 100 | return false; 101 | } 102 | 103 | if ($this->proxiedSession->isRegenerated()) { 104 | return true; 105 | } 106 | 107 | return $this->proxiedSession->hasChanged(); 108 | } 109 | 110 | /** 111 | * {@inheritDoc} 112 | * 113 | * @since 1.1.0 114 | */ 115 | public function getId() : string 116 | { 117 | $proxiedSession = $this->getProxiedSession(); 118 | return $proxiedSession instanceof SessionIdentifierAwareInterface 119 | ? $proxiedSession->getId() 120 | : ''; 121 | } 122 | 123 | /** 124 | * {@inheritDoc} 125 | * 126 | * @since 1.2.0 127 | */ 128 | public function persistSessionFor(int $duration) : void 129 | { 130 | $proxiedSession = $this->getProxiedSession(); 131 | if ($proxiedSession instanceof SessionCookiePersistenceInterface) { 132 | $proxiedSession->persistSessionFor($duration); 133 | } 134 | } 135 | 136 | /** 137 | * {@inheritDoc} 138 | * 139 | * @since 1.2.0 140 | */ 141 | public function getSessionLifetime() : int 142 | { 143 | $proxiedSession = $this->getProxiedSession(); 144 | return $proxiedSession instanceof SessionCookiePersistenceInterface 145 | ? $proxiedSession->getSessionLifetime() 146 | : 0; 147 | } 148 | 149 | public function initializeId(): string 150 | { 151 | if (! $this->persistence instanceof InitializePersistenceIdInterface) { 152 | throw NotInitializableException::invalidPersistence($this->persistence); 153 | } 154 | 155 | $this->proxiedSession = $this->persistence->initializeId($this->getProxiedSession()); 156 | return $this->proxiedSession->getId(); 157 | } 158 | 159 | 160 | private function getProxiedSession() : SessionInterface 161 | { 162 | if ($this->proxiedSession) { 163 | return $this->proxiedSession; 164 | } 165 | 166 | $this->proxiedSession = $this->persistence->initializeSessionFromRequest($this->request); 167 | return $this->proxiedSession; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Session.php: -------------------------------------------------------------------------------- 1 | data = $this->originalData = $data; 65 | $this->id = $id; 66 | 67 | if (isset($data[SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY])) { 68 | $this->sessionLifetime = $data[SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY]; 69 | } 70 | } 71 | 72 | /** 73 | * Convert a value to a JSON-serializable value. 74 | * 75 | * This value should be used by `set()` operations to ensure that the values 76 | * within a session are serializable across any session adapter. 77 | * 78 | * @param mixed $value 79 | * @return null|bool|int|float|string|array|\stdClass 80 | */ 81 | public static function extractSerializableValue($value) 82 | { 83 | return json_decode(json_encode($value, JSON_PRESERVE_ZERO_FRACTION), true); 84 | } 85 | 86 | /** 87 | * Retrieve all data for purposes of persistence. 88 | */ 89 | public function toArray() : array 90 | { 91 | return $this->data; 92 | } 93 | 94 | /** 95 | * @param mixed $default Default value to return if $name does not exist. 96 | * @return mixed 97 | */ 98 | public function get(string $name, $default = null) 99 | { 100 | return $this->data[$name] ?? $default; 101 | } 102 | 103 | public function has(string $name) : bool 104 | { 105 | return array_key_exists($name, $this->data); 106 | } 107 | 108 | /** 109 | * @param mixed $value 110 | */ 111 | public function set(string $name, $value) : void 112 | { 113 | $this->data[$name] = self::extractSerializableValue($value); 114 | } 115 | 116 | public function unset(string $name) : void 117 | { 118 | unset($this->data[$name]); 119 | } 120 | 121 | public function clear() : void 122 | { 123 | $this->data = []; 124 | } 125 | 126 | public function hasChanged() : bool 127 | { 128 | if ($this->isRegenerated) { 129 | return true; 130 | } 131 | 132 | return $this->data !== $this->originalData; 133 | } 134 | 135 | public function regenerate() : SessionInterface 136 | { 137 | $session = clone $this; 138 | $session->isRegenerated = true; 139 | return $session; 140 | } 141 | 142 | public function isRegenerated() : bool 143 | { 144 | return $this->isRegenerated; 145 | } 146 | 147 | /** 148 | * {@inheritDoc} 149 | * 150 | * @since 1.1.0 151 | */ 152 | public function getId() : string 153 | { 154 | return $this->id; 155 | } 156 | 157 | /** 158 | * {@inheritDoc} 159 | * 160 | * @since 1.2.0 161 | */ 162 | public function persistSessionFor(int $duration) : void 163 | { 164 | $this->sessionLifetime = $duration; 165 | $this->set(SessionCookiePersistenceInterface::SESSION_LIFETIME_KEY, $duration); 166 | } 167 | 168 | /** 169 | * {@inheritDoc} 170 | * 171 | * @since 1.2.0 172 | */ 173 | public function getSessionLifetime() : int 174 | { 175 | return $this->sessionLifetime; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/SessionCookiePersistenceInterface.php: -------------------------------------------------------------------------------- 1 | persistence = $persistence; 29 | } 30 | 31 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface 32 | { 33 | $session = new LazySession($this->persistence, $request); 34 | $response = $handler->handle($request->withAttribute(self::SESSION_ATTRIBUTE, $session)); 35 | return $this->persistence->persistSession($session, $response); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/SessionMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | get(SessionPersistenceInterface::class) 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/SessionPersistenceInterface.php: -------------------------------------------------------------------------------- 1 |