├── .github └── workflows │ └── continuous-integration.yml ├── .gitignore ├── .scrutinizer.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── codecov.yml ├── composer.json ├── docs ├── _bookdown.json └── getting-started.md ├── phpunit.php ├── phpunit.xml.dist ├── src ├── CsrfToken.php ├── CsrfTokenFactory.php ├── Exception.php ├── Exception │ └── SessionAlreadyStarted.php ├── Phpfunc.php ├── Randval.php ├── RandvalInterface.php ├── Segment.php ├── SegmentFactory.php ├── SegmentInterface.php ├── Session.php └── SessionFactory.php └── tests ├── CsrfTokenTest.php ├── FakePhpfunc.php ├── FakeSessionHandler.php ├── Issue23Test.php ├── SegmentTest.php ├── SessionFactoryTest.php └── SessionTest.php /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | operating-system: 16 | - ubuntu-latest 17 | php-version: 18 | - '7.2' 19 | - '7.3' 20 | - '7.4' 21 | - '8.0' 22 | - '8.1' 23 | - '8.2' 24 | - '8.3' 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v1 28 | 29 | - name: Setup PHP ${{ matrix.php-version }} 30 | uses: shivammathur/setup-php@v2 31 | with: 32 | php-version: ${{ matrix.php-version }} 33 | coverage: pcov 34 | tools: none 35 | ini-values: assert.exception=1, zend.assertions=1 36 | 37 | - name: Get composer cache directory 38 | id: composer-cache 39 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 40 | 41 | - name: Cache dependencies 42 | uses: actions/cache@v2 43 | with: 44 | path: ${{ steps.composer-cache.outputs.dir }} 45 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 46 | restore-keys: ${{ runner.os }}-composer- 47 | 48 | - name: Install dependencies 49 | run: composer install --no-interaction --prefer-dist 50 | 51 | - name: Run test suite 52 | run: ./vendor/bin/phpunit --coverage-clover=coverage.xml 53 | 54 | - name: Upload coverage report 55 | uses: codecov/codecov-action@v2 56 | with: 57 | fail_ci_if_error: false 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tests/container/vendor 2 | /tests/container/composer.* 3 | /composer.lock 4 | /vendor 5 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | nodes: 3 | analysis: 4 | tests: 5 | override: 6 | - php-scrutinizer-run 7 | filter: 8 | paths: ["src/*"] 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 4.0.0 4 | 5 | - PHP 7.2+ is now required. 6 | - Github CI workflow added. 7 | - Scrutinizer CI updated. 8 | - MIT License. 9 | 10 | ## 2.1.0 11 | 12 | - (ADD) Add support for hash_equals() in CsrfToken::isValid() 13 | - (ADD) Add support for random_bytes() in Randval::generate() 14 | - (TST) Update tests and test support files 15 | - (DOC) Update license year and remove branch alias 16 | 17 | ## 2.0.1 18 | 19 | This release modifies the testing structure and updates other support files. 20 | 21 | 22 | ## 2.0.0 23 | 24 | This is the first stable release of Aura.Session 2.0.0. 25 | 26 | - Fix #23 27 | 28 | - Merge pull request #33 from harikt/issue-23. 29 | 30 | - Merge pull request #35 from iansltx/php7-compat: Fix FakeSessionHandler::write() (fixes PHP7 tests) 31 | 32 | - Merge pull request #36 from fiahfy/spike: fix param type 33 | 34 | - Merge pull request #37 from tomkyle/develop-2: Removed redundant paragraph 35 | 36 | - Merge pull request #38 from tomkyle/develop-2: Clarified parameter descriptions 37 | 38 | - Updated documentation and support files. 39 | 40 | ## 2.0.0-beta2 41 | 42 | - TST: Update testing structure, use plain old PHPUnit for tests 43 | 44 | - CHG: Use new service naming rules 45 | 46 | - CHG: Disable auto-resolve for container tests 47 | 48 | ## 2.0.0-beta1 49 | 50 | First 2.0.0 beta release. 51 | 52 | ## 1.0.2 53 | 54 | Hygiene release. 55 | 56 | - Fix #8 related to unit tests failing because of ini_set values. Thanks @mindplay-dk 57 | 58 | - Merge pull request #12 from harikt/v2config, adds v2 config files. 59 | 60 | ## 1.0.1 61 | 62 | - [CHG] Manager::destroy() now checks whether the session is started; if not, 63 | starts it, and then destroys. This is because sessions are lazy-loading. 64 | 65 | - [DOC] Add PHP 5.5 to the Travis build and update docs 66 | 67 | ## 1.0.0 68 | 69 | There are BC breaks in this release, but it's a Google beta, so ... 70 | 71 | - [SEC] Based on conversation at 72 | http://www.eschrade.com/page/generating-secure-cross-site-request-forgery-tokens-csrf/ 73 | start using openssl and mcrypt for CSRF tokens instead of mt_rand(). 74 | 75 | - [NEW] SegmentInterface, Randval, RandvalInterface. 76 | 77 | - [BRK] The Manager now requires $_COOKIE as its third param to Manager. 78 | 79 | - [CHG] Segments now lazy-load themselves. On reads, they will reactivate an 80 | available session, but will not start a new one. On writes, they will 81 | reactivate an available session, or start a new one if one is not available. 82 | This means that creating a segment object no longer starts a session; you 83 | have to read from or write to one for the session to start. 84 | 85 | - [BRK] Renamed Manager::isActive() to isAvailable(), to differentiate from 86 | PHP_SESSION_ACTIVE. ( Previously, isActive() only told you if a session had 87 | started, not if one was available to be activated.) 88 | 89 | - [CHG] Manager::getSegment() no longer starts a session 90 | 91 | - [CHG] Manager::isStarted() now checks getStatus() for PHP_SESSION_ACTIVE 92 | instead of session_id(). 93 | 94 | - [CHG] Segment::__get() no longer returns a reference 95 | 96 | - [BRK] Renamed Manager::getSegment() to newSegment() 97 | 98 | - [CHG] Manager no longer retains session segments 99 | 100 | - [CHG] Various typo and doc fixes by Akihito Koriyama 101 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We are happy to review any contributions you want to make. When contributing, please follow the rules outlined at . 4 | 5 | The time between submitting a contribution and its review one may be extensive; do not be discouraged if there is not immediate feedback. 6 | 7 | Thanks! 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2022, Aura for PHP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aura Session 2 | 3 | Provides session management functionality, including lazy session starting, 4 | session segments, next-request-only ("flash") values, and CSRF tools. 5 | 6 | ## Foreword 7 | 8 | ### Installation 9 | 10 | This library requires PHP 7.2 or later. It has been tested on PHP 7.2-8.2. we recommend using the latest available version of PHP as a matter of principle. It has no userland dependencies. 11 | 12 | It is installable and autoloadable via Composer as [aura/session](https://packagist.org/packages/aura/session). 13 | 14 | Alternatively, [download a release](https://github.com/auraphp/Aura.Session/releases) or clone this repository, then require or include its _autoload.php_ file. 15 | 16 | ### Quality 17 | 18 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/auraphp/Aura.Session/badges/quality-score.png?b=4.x)](https://scrutinizer-ci.com/g/auraphp/Aura.Session/?branch=4.x) 19 | [![codecov](https://codecov.io/gh/auraphp/Aura.Session/branch/4.x/graph/badge.svg)](https://codecov.io/gh/auraphp/Aura.Session) 20 | [![Continuous Integration](https://github.com/auraphp/Aura.Session/actions/workflows/continuous-integration.yml/badge.svg?branch=4.x)](https://github.com/auraphp/Aura.Session/actions/workflows/continuous-integration.yml) 21 | 22 | To run the unit tests at the command line, issue `composer install` and then `phpunit` at the package root. This requires [Composer](http://getcomposer.org/) to be available as `composer`, and [PHPUnit](http://phpunit.de/manual/) to be available as `phpunit`. 23 | 24 | This library attempts to comply with [PSR-1][], [PSR-2][], and [PSR-4][]. If 25 | you notice compliance oversights, please send a patch via pull request. 26 | 27 | [PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md 28 | [PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md 29 | [PSR-4]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md 30 | 31 | ### Community 32 | 33 | To ask questions, provide feedback, or otherwise communicate with the Aura community, please join our [Google Group](http://groups.google.com/group/auraphp), follow [@auraphp on Twitter](http://twitter.com/auraphp), or chat with us on #auraphp on Freenode. 34 | 35 | 36 | ## Documentation 37 | 38 | This package is fully documented [here](./docs/getting-started.md). 39 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aura/session", 3 | "type": "library", 4 | "description": "Provides session management functionality, including lazy session starting, session segments, next-request-only (\"flash\") values, and CSRF tools.", 5 | "keywords": [ 6 | "session", 7 | "sessions", 8 | "flash", 9 | "flash message", 10 | "csrf" 11 | ], 12 | "homepage": "https://github.com/auraphp/Aura.Session", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Aura.Session Contributors", 17 | "homepage": "https://github.com/auraphp/Aura.Session/contributors" 18 | } 19 | ], 20 | "require": { 21 | "php": "^7.2 || ^8.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Aura\\Session\\": "src/" 26 | } 27 | }, 28 | "suggest": { 29 | "ext-openssl": "OpenSSL generates the best secure CSRF tokens.", 30 | "ext-mcrypt": "Mcrypt generates the next best secure CSRF tokens." 31 | }, 32 | "require-dev": { 33 | "phpunit/phpunit": "^8.5" 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Aura\\Session\\": "tests/" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/_bookdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Aura.Session", 3 | "content": [ 4 | "getting-started.md" 5 | ] 6 | } -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | ### Instantiation 4 | 5 | The easiest way to get started is to use the _SessionFactory_ to create a _Session_ manager object. 6 | 7 | ```php 8 | newInstance($_COOKIE); 11 | ?> 12 | ``` 13 | 14 | We can then use the _Session_ instance to create _Segment_ objects to manage session values and flashes. (In general, we should not need to manipulate the _Session_ manager directly -- we will work mostly with _Segment_ objects.) 15 | 16 | ### Segments 17 | 18 | In normal PHP, we keep session values in the `$_SESSION` array. However, when different libraries and projects try to modify the same keys, the resulting conflicts can result in unexpected behavior. To resolve this, we use _Segment_ objects. Each _Segment_ addresses a named key within the `$_SESSION` array for deconfliction purposes. 19 | 20 | For example, if we get a _Segment_ for `Vendor\Package\ClassName`, that _Segment_ will contain a reference to `$_SESSION['Vendor\Package\ClassName']`. We can then `set()` and `get()` values on the _Segment_, and the values will reside in an array under that reference. 21 | 22 | ```php 23 | getSegment('Vendor\Package\ClassName'); 26 | 27 | // try to get a value from the segment; 28 | // if it does not exist, return an alternative value 29 | echo $segment->get('foo'); // null 30 | echo $segment->get('baz', 'not set'); // 'not set' 31 | 32 | // set some values on the segment 33 | $segment->set('foo', 'bar'); 34 | $segment->set('baz', 'dib'); 35 | 36 | // the $_SESSION array is now: 37 | // $_SESSION = array( 38 | // 'Vendor\Package\ClassName' => array( 39 | // 'foo' => 'bar', 40 | // 'baz' => 'dib', 41 | // ), 42 | // ); 43 | 44 | // try again to get a value from the segment 45 | echo $segment->get('foo'); // 'bar' 46 | 47 | // because the segment is a reference to $_SESSION, we can modify 48 | // the superglobal directly and the segment values will also change 49 | $_SESSION['Vendor\Package\ClassName']['zim'] = 'gir' 50 | echo $segment->get('zim'); // 'gir' 51 | ?> 52 | ``` 53 | 54 | The benefit of a session segment is that we can deconflict the keys in the 55 | `$_SESSION` superglobal by using class names (or some other unique name) for 56 | the segment names. With segments, different packages can use the `$_SESSION` 57 | superglobal without stepping on each other's toes. 58 | 59 | To clear all the values on a _Segment_, use the `clear()` method. 60 | 61 | ### Flash Values 62 | 63 | _Segment_ values persist until the session is cleared or destroyed. However, sometimes it is useful to set a value that propagates only through the next request, and is then discarded. These are called "flash" values. 64 | 65 | #### Setting And Getting Flash Values 66 | 67 | To set a flash value on a _Segment_, use the `setFlash()` method. 68 | 69 | ```php 70 | getSegment('Vendor\Package\ClassName'); 72 | $segment->setFlash('message', 'Hello world!'); 73 | ?> 74 | ``` 75 | 76 | Then, in subsequent requests, we can read the flash value using `getFlash()`: 77 | 78 | ```php 79 | getSegment('Vendor\Package\ClassName'); 81 | $message = $segment->getFlash('message'); // 'Hello world!' 82 | ?> 83 | ``` 84 | 85 | > N.b. As with `get()`, we can provide an alternative value if the flash key does not exist. For example, `getFlash('foo', 'not set')` will return 'not set' if there is no 'foo' key available. 86 | 87 | Using `setFlash()` makes the flash value available only in the *next* request, not the current one. To make the flash value available immediately as well as in the next request, use `setFlashNow($key, $val)`. 88 | 89 | Using `getFlash()` returns only the values that are available now from having been set in the previous request. To read a value that will be available in the next request, use `getFlashNext($key, $alt)`. 90 | 91 | #### Keeping and Clearing Flash Values 92 | 93 | Sometimes we will want to keep the flash values in the current request for the next request. We can do so on a per-segment basis by calling the _Segment_ `keepFlash()` method. 94 | 95 | Similarly, we can clear flash values just for that _Segment_ with `clearFlash()` method. 96 | 97 | ### Lazy Session Starting 98 | 99 | Merely instantiating the _Session_ manager and getting a _Segment_ from it does *not* call `session_start()`. Instead, `session_start()` occurs only in certain circumstances: 100 | 101 | - If we *read* from a _Segment_ (e.g. with `get()`) the _Session_ looks to see if a session cookie has already been set. If so, it will call `session_start()` to resume the previously-started session. If not, it knows there are no previously existing `$_SESSION` values, so it will not call `session_start()`. 102 | 103 | - If we *write* to a _Segment_ (e.g. with `set()`) the _Session_ will always call `session_start()`. This will resume a previous session if it exists, or start a new one if it does not. 104 | 105 | This means we can create each _Segment_ at will, and `session_start()` will not be invoked until we actually interact with a _Segment_ in a particular way. This helps to conserve the resources involved in starting a session. 106 | 107 | Of course, we can force a session start or reactivation by calling the _Session_ `start()` method, but that defeats the purpose of lazy-loaded sessions. 108 | 109 | 110 | ### Saving, Clearing, and Destroying Sessions 111 | 112 | > N.b.: These methods apply to all session data and flashes across all segments. 113 | 114 | To save the session data and end its use during the current request, call the `commit()` method on the _Session_ manager: 115 | 116 | ```php 117 | commit(); 119 | ?> 120 | ``` 121 | 122 | > N.b.: Per , "Sessions 123 | > normally shutdown automatically when PHP is finished executing a script, but 124 | > can be manually shutdown using the session_write_close() function." 125 | > The `commit()` method is the equivalent of `session_write_close()`. 126 | 127 | To clear all session data, but leave the session active during the current request, use the `clear()` method on the _Session_ manager. 128 | 129 | ```php 130 | clear(); 132 | ?> 133 | ``` 134 | 135 | To clear all flash values on a segment, use the `clearFlash()` method: 136 | 137 | To clear the data *and* terminate the session for this and future requests, thereby destroying it completely, call the `destroy()` method: 138 | 139 | ```php 140 | destroy(); // equivalent of session_destroy() 142 | ?> 143 | ``` 144 | 145 | Calling `destroy()` will also delete the session cookie via `setcookie()`. If we have an alternative means by which we delete cookies, we should pass a callable as the second argument to the _SessionFactory_ method `newInstance()`. The callable should take three parameters: the cookie name, path, and domain. 146 | 147 | ```php 148 | cookies->delete($name, $path, $domain); 153 | } 154 | 155 | $session = $session_factory->newInstance($_COOKIE, $delete_cookie); 156 | ?> 157 | ``` 158 | 159 | ## Session Security 160 | 161 | ### Session ID Regeneration 162 | 163 | Any time a user has a change in privilege (that is, gaining or losing access 164 | rights within a system) be sure to regenerate the session ID: 165 | 166 | ```php 167 | regenerateId(); 169 | ?> 170 | ``` 171 | 172 | > N.b.: The `regenerateId()` method also regenerates the CSRF token value. 173 | 174 | ### Cross-Site Request Forgery 175 | 176 | A "cross-site request forgery" is a security issue where the attacker, via 177 | malicious JavaScript or other means, issues a request in-the-blind from a 178 | client browser to a server where the user has already authenticated. The 179 | request *looks* valid to the server, but in fact is a forgery, since the user 180 | did not actually make the request (the malicious JavaScript did). 181 | 182 | 183 | 184 | #### Defending Against CSRF 185 | 186 | To defend against CSRF attacks, server-side logic should: 187 | 188 | 1. Place a token value unique to each authenticated user session in each form; 189 | and 190 | 191 | 2. Check that all incoming POST/PUT/DELETE (i.e., "unsafe") requests contain 192 | that value. 193 | 194 | > N.b.: If our application uses GET requests to modify resources (which 195 | > incidentally is an improper use of GET), we should also check for CSRF on 196 | > GET requests from authenticated users. 197 | 198 | For this example, the form field name will be `__csrf_value`. In each form 199 | we want to protect against CSRF, we use the session CSRF token value for that 200 | field: 201 | 202 | ```php 203 | 209 |
210 | 211 | auth->isValid()) { 212 | $csrf_value = $session->getCsrfToken()->getValue(); 213 | echo ''; 216 | } ?> 217 | 218 | 219 | 220 |
221 | ``` 222 | 223 | When processing the request, check to see if the incoming CSRF token is valid 224 | for the authenticated user: 225 | 226 | ```php 227 | auth->isValid()) { 238 | $csrf_value = $_POST['__csrf_value']; 239 | $csrf_token = $session->getCsrfToken(); 240 | if (! $csrf_token->isValid($csrf_value)) { 241 | echo "This looks like a cross-site request forgery."; 242 | } else { 243 | echo "This looks like a valid request."; 244 | } 245 | } else { 246 | echo "CSRF attacks only affect unsafe requests by authenticated users."; 247 | } 248 | ?> 249 | ``` 250 | 251 | > Note : By default the above code only works for single form. If you need 252 | multiple tokens, pass different keys to `getValue` and `isValid` methods. 253 | For examples please look into the unit test class `CsrfTokenTest`. 254 | 255 | #### CSRF Value Generation 256 | 257 | For a CSRF token to be useful, its random value must be cryptographically 258 | secure. Aura.Session comes with a `Randval` class that implements a 259 | `RandvalInterface`, and uses default `random_bytes` to generate a 260 | random value. If you are not satisified, 261 | you can create your own implementation of the `RandvalInterface`. 262 | 263 | ### Session Lifetime 264 | 265 | We can set the session lifetime to as long (or as short) as we like using the `setCookieParams` on _Session_ object. The lifetime is in seconds. To set the session cookie lifetime to two weeks: 266 | 267 | ``` 268 | setCookieParams(array('lifetime' => '1209600')); 270 | ?> 271 | ``` 272 | 273 | > N.b: The `setCookieParams` method calls [session_set_cookie_params](http://php.net/session_set_cookie_params) internally. Thus, you need to call `setCookieParams` for every request and before session_start() is called. 274 | -------------------------------------------------------------------------------- /phpunit.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests 5 | 6 | 7 | 8 | 9 | ./src 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/CsrfToken.php: -------------------------------------------------------------------------------- 1 | segment = $segment; 51 | $this->randval = $randval; 52 | } 53 | 54 | /** 55 | * 56 | * Checks whether an incoming CSRF token value is valid. 57 | * 58 | * @param string $value The incoming token value. 59 | * 60 | * @param string $key A string key name which session value is saved. 61 | * 62 | * @return bool True if valid, false if not. 63 | * 64 | */ 65 | public function isValid($value, $key = 'value') 66 | { 67 | return hash_equals($value, $this->getValue($key)); 68 | } 69 | 70 | /** 71 | * 72 | * Gets the value of the outgoing CSRF token. 73 | * 74 | * @param string $key A string key name which session value is saved. 75 | * 76 | * @return string 77 | * 78 | */ 79 | public function getValue($key = 'value') 80 | { 81 | if ($this->segment->get($key) == null ) { 82 | $this->regenerateValue($key); 83 | } 84 | 85 | return $this->segment->get($key); 86 | } 87 | 88 | /** 89 | * 90 | * Regenerates the value of the outgoing CSRF token. 91 | * 92 | * @param string $key A string key name which session value is saved. 93 | * 94 | * @return string 95 | * 96 | */ 97 | public function regenerateValue($key = 'value') 98 | { 99 | $hash = hash('sha512', $this->randval->generate()); 100 | $this->segment->set($key, $hash); 101 | 102 | return $this->segment->get($key); 103 | } 104 | 105 | 106 | /** 107 | * Regenerates all csrf tokens 108 | * 109 | * @return void 110 | */ 111 | public function regenerateAllKeyValues() 112 | { 113 | $segment = $this->segment->getSegment(); 114 | 115 | if ($segment) { 116 | foreach ($segment as $key => $value) { 117 | $this->regenerateValue($key); 118 | } 119 | } 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/CsrfTokenFactory.php: -------------------------------------------------------------------------------- 1 | randval = $randval; 40 | } 41 | 42 | /** 43 | * 44 | * Creates a CsrfToken object. 45 | * 46 | * @param Session $session The session manager. 47 | * 48 | * @return CsrfToken 49 | * 50 | */ 51 | public function newInstance(Session $session) 52 | { 53 | $segment = $session->getSegment('Aura\Session\CsrfToken'); 54 | return new CsrfToken($segment, $this->randval); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | session = $session; 50 | $this->name = $name; 51 | } 52 | 53 | /** 54 | * 55 | * Returns the value of a key in the segment. 56 | * 57 | * @param string $key The key in the segment. 58 | * 59 | * @param mixed $alt An alternative value to return if the key is not set. 60 | * 61 | * @return mixed 62 | * 63 | */ 64 | public function get($key, $alt = null) 65 | { 66 | $this->resumeSession(); 67 | return isset($_SESSION[$this->name][$key]) 68 | ? $_SESSION[$this->name][$key] 69 | : $alt; 70 | } 71 | 72 | /** 73 | * 74 | * Returns the entire segment. 75 | * 76 | * @return mixed 77 | * 78 | */ 79 | public function getSegment() 80 | { 81 | $this->resumeSession(); 82 | return isset($_SESSION[$this->name]) 83 | ? $_SESSION[$this->name] 84 | : null; 85 | } 86 | 87 | /** 88 | * 89 | * Sets the value of a key in the segment. 90 | * 91 | * @param string $key The key to set. 92 | * 93 | * @param mixed $val The value to set it to. 94 | * 95 | */ 96 | public function set($key, $val) 97 | { 98 | $this->resumeOrStartSession(); 99 | $_SESSION[$this->name][$key] = $val; 100 | } 101 | 102 | /** 103 | * 104 | * Clear all data from the segment. 105 | * 106 | * @return null 107 | * 108 | */ 109 | public function clear() 110 | { 111 | if ($this->resumeSession()) { 112 | $_SESSION[$this->name] = array(); 113 | } 114 | } 115 | 116 | 117 | /** 118 | * Remove a key from the segment, or remove the entire segment (including key) from the session 119 | * 120 | * @param null $key 121 | */ 122 | public function remove($key = null) { 123 | if ($this->resumeSession()) { 124 | if($key){ 125 | if(isset($_SESSION[$this->name]) && array_key_exists($key, $_SESSION[$this->name])){ 126 | unset($_SESSION[$this->name][$key]); 127 | } 128 | } else { 129 | unset($_SESSION[$this->name]); 130 | } 131 | } 132 | } 133 | 134 | /** 135 | * 136 | * Sets a flash value for the *next* request. 137 | * 138 | * @param string $key The key for the flash value. 139 | * 140 | * @param mixed $val The flash value itself. 141 | * 142 | */ 143 | public function setFlash($key, $val) 144 | { 145 | $this->resumeOrStartSession(); 146 | $_SESSION[Session::FLASH_NEXT][$this->name][$key] = $val; 147 | } 148 | 149 | /** 150 | * 151 | * Gets the flash value for a key in the *current* request. 152 | * 153 | * @param string $key The key for the flash value. 154 | * 155 | * @param mixed $alt An alternative value to return if the key is not set. 156 | * 157 | * @return mixed The flash value itself. 158 | * 159 | */ 160 | public function getFlash($key, $alt = null) 161 | { 162 | $this->resumeSession(); 163 | return isset($_SESSION[Session::FLASH_NOW][$this->name][$key]) 164 | ? $_SESSION[Session::FLASH_NOW][$this->name][$key] 165 | : $alt; 166 | } 167 | 168 | /** 169 | * 170 | * Clears flash values for *only* the next request. 171 | * 172 | * @return null 173 | * 174 | */ 175 | public function clearFlash() 176 | { 177 | if ($this->resumeSession()) { 178 | $_SESSION[Session::FLASH_NEXT][$this->name] = array(); 179 | } 180 | } 181 | 182 | /** 183 | * 184 | * Gets the flash value for a key in the *next* request. 185 | * 186 | * @param string $key The key for the flash value. 187 | * 188 | * @param mixed $alt An alternative value to return if the key is not set. 189 | * 190 | * @return mixed The flash value itself. 191 | * 192 | */ 193 | public function getFlashNext($key, $alt = null) 194 | { 195 | $this->resumeSession(); 196 | return isset($_SESSION[Session::FLASH_NEXT][$this->name][$key]) 197 | ? $_SESSION[Session::FLASH_NEXT][$this->name][$key] 198 | : $alt; 199 | } 200 | 201 | /** 202 | * 203 | * Sets a flash value for the *next* request *and* the current one. 204 | * 205 | * @param string $key The key for the flash value. 206 | * 207 | * @param mixed $val The flash value itself. 208 | * 209 | */ 210 | public function setFlashNow($key, $val) 211 | { 212 | $this->resumeOrStartSession(); 213 | $_SESSION[Session::FLASH_NOW][$this->name][$key] = $val; 214 | $_SESSION[Session::FLASH_NEXT][$this->name][$key] = $val; 215 | } 216 | 217 | /** 218 | * 219 | * Clears flash values for *both* the next request *and* the current one. 220 | * 221 | * @return null 222 | * 223 | */ 224 | public function clearFlashNow() 225 | { 226 | if ($this->resumeSession()) { 227 | $_SESSION[Session::FLASH_NOW][$this->name] = array(); 228 | $_SESSION[Session::FLASH_NEXT][$this->name] = array(); 229 | } 230 | } 231 | 232 | /** 233 | * 234 | * Retains all the current flash values for the next request; values that 235 | * already exist for the next request take precedence. 236 | * 237 | * @return null 238 | * 239 | */ 240 | public function keepFlash() 241 | { 242 | if ($this->resumeSession()) { 243 | $_SESSION[Session::FLASH_NEXT][$this->name] = array_merge( 244 | $_SESSION[Session::FLASH_NEXT][$this->name], 245 | $_SESSION[Session::FLASH_NOW][$this->name] 246 | ); 247 | } 248 | } 249 | 250 | /** 251 | * 252 | * Loads the segment only if the session has already been started, or if 253 | * a session is available (in which case it resumes the session first). 254 | * 255 | * @return bool 256 | * 257 | */ 258 | protected function resumeSession() 259 | { 260 | if ($this->session->isStarted() || $this->session->resume()) { 261 | $this->load(); 262 | return true; 263 | } 264 | 265 | return false; 266 | } 267 | 268 | /** 269 | * 270 | * Sets the segment properties to $_SESSION references. 271 | * 272 | * @return null 273 | * 274 | */ 275 | protected function load() 276 | { 277 | if (! isset($_SESSION[$this->name])) { 278 | $_SESSION[$this->name] = array(); 279 | } 280 | 281 | if (! isset($_SESSION[Session::FLASH_NOW][$this->name])) { 282 | $_SESSION[Session::FLASH_NOW][$this->name] = array(); 283 | } 284 | 285 | if (! isset($_SESSION[Session::FLASH_NEXT][$this->name])) { 286 | $_SESSION[Session::FLASH_NEXT][$this->name] = array(); 287 | } 288 | } 289 | 290 | /** 291 | * 292 | * Resumes a previous session, or starts a new one, and loads the segment. 293 | * 294 | * @return null 295 | * 296 | */ 297 | protected function resumeOrStartSession() 298 | { 299 | if (! $this->resumeSession()) { 300 | $this->session->start(); 301 | $this->load(); 302 | } 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/SegmentFactory.php: -------------------------------------------------------------------------------- 1 | segment_factory = $segment_factory; 147 | $this->csrf_token_factory = $csrf_token_factory; 148 | $this->phpfunc = $phpfunc; 149 | $this->cookies = $cookies; 150 | 151 | $this->setDeleteCookie($delete_cookie); 152 | 153 | $this->cookie_params = $this->phpfunc->session_get_cookie_params(); 154 | } 155 | 156 | /** 157 | * 158 | * Sets the delete-cookie callable. 159 | * 160 | * If parameter is `null`, the session cookie will be deleted using the 161 | * traditional way, i.e. using an expiration date in the past. 162 | * 163 | * @param callable|null $delete_cookie The callable to invoke when deleting the 164 | * session cookie. 165 | * 166 | */ 167 | public function setDeleteCookie($delete_cookie) 168 | { 169 | $this->delete_cookie = $delete_cookie; 170 | if (! $this->delete_cookie) { 171 | $phpfunc = $this->phpfunc; 172 | $this->delete_cookie = function ( 173 | $name, 174 | $params 175 | ) use ($phpfunc) { 176 | $phpfunc->setcookie( 177 | $name, 178 | '', 179 | time() - 42000, 180 | $params['path'], 181 | $params['domain'] 182 | ); 183 | }; 184 | } 185 | } 186 | 187 | /** 188 | * 189 | * Gets a new session segment instance by name. Segments with the same 190 | * name will be different objects but will reference the same $_SESSION 191 | * values, so it is possible to have two or more objects that share state. 192 | * For good or bad, this a function of how $_SESSION works. 193 | * 194 | * @param string $name The name of the session segment, typically a 195 | * fully-qualified class name. 196 | * 197 | * @return Segment New Segment instance. 198 | * 199 | */ 200 | public function getSegment($name) 201 | { 202 | return $this->segment_factory->newInstance($this, $name); 203 | } 204 | 205 | /** 206 | * 207 | * Is a session available to be resumed? 208 | * 209 | * @return bool 210 | * 211 | */ 212 | public function isResumable() 213 | { 214 | $name = $this->getName(); 215 | return isset($this->cookies[$name]); 216 | } 217 | 218 | /** 219 | * 220 | * Is the session already started? 221 | * 222 | * @return bool 223 | * 224 | */ 225 | public function isStarted() 226 | { 227 | if ($this->phpfunc->function_exists('session_status')) { 228 | $started = $this->phpfunc->session_status() === PHP_SESSION_ACTIVE; 229 | } else { 230 | $started = $this->sessionStatus(); 231 | } 232 | 233 | // if the session was started externally, move the flash values forward 234 | if ($started && ! $this->flash_moved) { 235 | $this->moveFlash(); 236 | } 237 | 238 | // done 239 | return $started; 240 | } 241 | 242 | /** 243 | * 244 | * Returns the session status. 245 | * 246 | * Nota bene: 247 | * 248 | * PHP 5.3 implementation of session_status() for only active/none. 249 | * Relies on the fact that ini setting 'session.use_trans_sid' cannot be 250 | * changed when a session is active. 251 | * 252 | * PHP ini_set() raises a warning when we attempt to change this setting 253 | * and session is active. Note that the attempted change is to the 254 | * pre-existing value, so nothing will actually change on success. 255 | * 256 | */ 257 | protected function sessionStatus() 258 | { 259 | $setting = 'session.use_trans_sid'; 260 | $current = $this->phpfunc->ini_get($setting); 261 | $level = $this->phpfunc->error_reporting(0); 262 | $result = $this->phpfunc->ini_set($setting, $current); 263 | $this->phpfunc->error_reporting($level); 264 | return $result !== $current; 265 | } 266 | 267 | /** 268 | * 269 | * Starts a new or existing session. 270 | * 271 | * @return bool 272 | * 273 | */ 274 | public function start() 275 | { 276 | $result = $this->phpfunc->session_start(); 277 | if ($result && ! $this->flash_moved) { 278 | $this->moveFlash(); 279 | } 280 | return $result; 281 | } 282 | 283 | /** 284 | * 285 | * Moves the "next" flash values to the "now" values, thereby clearing the 286 | * "next" values. 287 | * 288 | * @return null 289 | * 290 | */ 291 | protected function moveFlash() 292 | { 293 | if (! isset($_SESSION[Session::FLASH_NEXT])) { 294 | $_SESSION[Session::FLASH_NEXT] = array(); 295 | } 296 | $_SESSION[Session::FLASH_NOW] = $_SESSION[Session::FLASH_NEXT]; 297 | $_SESSION[Session::FLASH_NEXT] = array(); 298 | $this->flash_moved = true; 299 | } 300 | 301 | /** 302 | * 303 | * Resumes a session, but does not start a new one if there is no 304 | * existing one. 305 | * 306 | * @return bool 307 | * 308 | */ 309 | public function resume() 310 | { 311 | if ($this->isStarted()) { 312 | return true; 313 | } 314 | 315 | if ($this->isResumable()) { 316 | return $this->start(); 317 | } 318 | 319 | return false; 320 | } 321 | 322 | /** 323 | * 324 | * Clears all session variables across all segments. 325 | * 326 | * @return null 327 | * 328 | */ 329 | public function clear() 330 | { 331 | return $this->phpfunc->session_unset(); 332 | } 333 | 334 | /** 335 | * 336 | * Writes session data from all segments and ends the session. 337 | * 338 | * @return null 339 | * 340 | */ 341 | public function commit() 342 | { 343 | return $this->phpfunc->session_write_close(); 344 | } 345 | 346 | /** 347 | * 348 | * Destroys the session entirely. 349 | * 350 | * @return bool 351 | * 352 | * @see http://php.net/manual/en/function.session-destroy.php 353 | * 354 | */ 355 | public function destroy() 356 | { 357 | if (! $this->isStarted()) { 358 | $this->start(); 359 | } 360 | 361 | $name = $this->getName(); 362 | $params = $this->getCookieParams(); 363 | $this->clear(); 364 | 365 | $destroyed = $this->phpfunc->session_destroy(); 366 | if ($destroyed) { 367 | call_user_func($this->delete_cookie, $name, $params); 368 | } 369 | 370 | return $destroyed; 371 | } 372 | 373 | /** 374 | * 375 | * Returns the CSRF token, creating it if needed (and thereby starting a 376 | * session). 377 | * 378 | * @return CsrfToken 379 | * 380 | */ 381 | public function getCsrfToken() 382 | { 383 | if (! $this->csrf_token) { 384 | $this->csrf_token = $this->csrf_token_factory->newInstance($this); 385 | } 386 | 387 | return $this->csrf_token; 388 | } 389 | 390 | // ======================================================================= 391 | // 392 | // support and admin methods 393 | // 394 | 395 | /** 396 | * 397 | * Sets the session cache expire time. 398 | * 399 | * @param int $expire The expiration time in seconds. 400 | * 401 | * @return int 402 | * 403 | * @see session_cache_expire() 404 | * 405 | */ 406 | public function setCacheExpire($expire) 407 | { 408 | return $this->phpfunc->session_cache_expire($expire); 409 | } 410 | 411 | /** 412 | * 413 | * Gets the session cache expire time. 414 | * 415 | * @return int The cache expiration time in seconds. 416 | * 417 | * @see session_cache_expire() 418 | * 419 | */ 420 | public function getCacheExpire() 421 | { 422 | return $this->phpfunc->session_cache_expire(); 423 | } 424 | 425 | /** 426 | * 427 | * Sets the session cache limiter value. 428 | * 429 | * @param string $limiter The limiter value. 430 | * 431 | * @return string 432 | * 433 | * @see session_cache_limiter() 434 | * 435 | */ 436 | public function setCacheLimiter($limiter) 437 | { 438 | return $this->phpfunc->session_cache_limiter($limiter); 439 | } 440 | 441 | /** 442 | * 443 | * Gets the session cache limiter value. 444 | * 445 | * @return string The limiter value. 446 | * 447 | * @see session_cache_limiter() 448 | * 449 | */ 450 | public function getCacheLimiter() 451 | { 452 | return $this->phpfunc->session_cache_limiter(); 453 | } 454 | 455 | /** 456 | * 457 | * Sets the session cookie params. Param array keys are: 458 | * 459 | * - `lifetime` : Lifetime of the session cookie, defined in seconds. 460 | * 461 | * - `path` : Path on the domain where the cookie will work. 462 | * Use a single slash ('/') for all paths on the domain. 463 | * 464 | * - `domain` : Cookie domain, for example 'www.php.net'. 465 | * To make cookies visible on all subdomains then the domain must be 466 | * prefixed with a dot like '.php.net'. 467 | * 468 | * - `secure` : If TRUE cookie will only be sent over secure connections. 469 | * 470 | * - `httponly` : If set to TRUE then PHP will attempt to send the httponly 471 | * flag when setting the session cookie. 472 | * 473 | * - `samesite` : Set if the cookie should be restricted to a first-party or same-site context. 474 | * Possible values are 'Lax', 'Strict' or 'None'. 475 | * 476 | * @param array $params The array of session cookie param keys and values. 477 | * 478 | * @return null 479 | * 480 | * @throws SessionAlreadyStarted 481 | * 482 | * @see session_set_cookie_params() 483 | * 484 | */ 485 | public function setCookieParams(array $params) 486 | { 487 | if ($this->isStarted()) { 488 | throw new SessionAlreadyStarted(); 489 | } 490 | 491 | $this->cookie_params = array_merge($this->cookie_params, $params); 492 | if (PHP_VERSION_ID < 70300) { 493 | $this->phpfunc->session_set_cookie_params( 494 | $this->cookie_params['lifetime'], 495 | $this->cookie_params['path'], 496 | $this->cookie_params['domain'], 497 | $this->cookie_params['secure'], 498 | $this->cookie_params['httponly'] 499 | ); 500 | } else { 501 | $this->phpfunc->session_set_cookie_params($this->cookie_params); 502 | } 503 | } 504 | 505 | /** 506 | * 507 | * Gets the session cookie params. 508 | * 509 | * @return array 510 | * 511 | */ 512 | public function getCookieParams() 513 | { 514 | return $this->cookie_params; 515 | } 516 | 517 | /** 518 | * 519 | * Gets the current session id. 520 | * 521 | * @return string 522 | * 523 | */ 524 | public function getId() 525 | { 526 | return $this->phpfunc->session_id(); 527 | } 528 | 529 | /** 530 | * 531 | * Regenerates and replaces the current session id; also regenerates the 532 | * CSRF token value if one exists. 533 | * 534 | * @return bool True if regeneration worked, false if not. 535 | * 536 | */ 537 | public function regenerateId() 538 | { 539 | $result = $this->phpfunc->session_regenerate_id(true); 540 | if ($result && $this->csrf_token) { 541 | $this->csrf_token->regenerateAllKeyValues(); 542 | } 543 | return $result; 544 | } 545 | 546 | /** 547 | * 548 | * Sets the current session name. 549 | * 550 | * @param string $name The session name to use. 551 | * 552 | * @return string 553 | * 554 | * @see session_name() 555 | * 556 | */ 557 | public function setName($name) 558 | { 559 | return $this->phpfunc->session_name($name); 560 | } 561 | 562 | /** 563 | * 564 | * Returns the current session name. 565 | * 566 | * @return string 567 | * 568 | */ 569 | public function getName() 570 | { 571 | return $this->phpfunc->session_name(); 572 | } 573 | 574 | /** 575 | * 576 | * Sets the session save path. 577 | * 578 | * @param string $path The new save path. 579 | * 580 | * @return string 581 | * 582 | * @see session_save_path() 583 | * 584 | */ 585 | public function setSavePath($path) 586 | { 587 | return $this->phpfunc->session_save_path($path); 588 | } 589 | 590 | /** 591 | * 592 | * Gets the session save path. 593 | * 594 | * @return string 595 | * 596 | * @see session_save_path() 597 | * 598 | */ 599 | public function getSavePath() 600 | { 601 | return $this->phpfunc->session_save_path(); 602 | } 603 | } 604 | -------------------------------------------------------------------------------- /src/SessionFactory.php: -------------------------------------------------------------------------------- 1 | phpfunc = new FakePhpfunc; 22 | 23 | $this->session = new Session( 24 | new SegmentFactory, 25 | new CsrfTokenFactory(new Randval()), 26 | $this->phpfunc, 27 | $_COOKIE 28 | ); 29 | } 30 | 31 | public function teardown(): void 32 | { 33 | session_unset(); 34 | if (session_id() !== '') { 35 | session_destroy(); 36 | } 37 | } 38 | 39 | public function testLaziness() 40 | { 41 | $this->assertFalse($this->session->isStarted()); 42 | $token = $this->session->getCsrfToken(); 43 | $this->assertFalse($this->session->isStarted()); 44 | $token->getValue('__csrf'); 45 | $this->assertTrue($this->session->isStarted()); 46 | } 47 | 48 | public function testGetAndRegenerateValue() 49 | { 50 | $token = $this->session->getCsrfToken(); 51 | 52 | $old = $token->getValue(); 53 | $this->assertTrue($old != ''); 54 | 55 | // with openssl 56 | $this->phpfunc->extensions = array('openssl'); 57 | $token->regenerateValue(); 58 | $openssl = $token->getValue(); 59 | $this->assertTrue($old != $openssl); 60 | 61 | // with mcrypt 62 | $this->phpfunc->extensions = array('mcrypt'); 63 | $token->regenerateValue(); 64 | $mcrypt = $token->getValue(); 65 | $this->assertTrue($old != $openssl && $old != $mcrypt); 66 | 67 | if (!$this->phpfunc->function_exists('random_bytes')) { 68 | // with nothing 69 | $this->phpfunc->extensions = array(); 70 | $this->expectException('Aura\Session\Exception'); 71 | $token->regenerateValue(); 72 | } 73 | 74 | } 75 | 76 | public function testIsValid() 77 | { 78 | $token = $this->session->getCsrfToken(); 79 | $value = $token->getValue(); 80 | 81 | $this->assertTrue($token->isValid($value)); 82 | $token->regenerateValue(); 83 | $this->assertFalse($token->isValid($value)); 84 | } 85 | 86 | public function testDifferentTokens() 87 | { 88 | $this->assertFalse($this->session->isStarted()); 89 | $token = $this->session->getCsrfToken(); 90 | 91 | $value1 = $token->getValue('__csrf1'); 92 | $value2 = $token->getValue('__csrf2'); 93 | $value3 = $token->getValue('__csrf3'); 94 | 95 | $this->assertTrue($token->isValid($value1, '__csrf1')); 96 | $this->assertTrue($token->isValid($value2, '__csrf2')); 97 | $this->assertTrue($token->isValid($value3, '__csrf3')); 98 | 99 | // After isValid call, the value stored in session will not be reset 100 | $this->assertEquals($value3, $token->getValue('__csrf3')); 101 | 102 | $this->assertNotEquals($value3, $token->regenerateValue('__csrf3')); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/FakePhpfunc.php: -------------------------------------------------------------------------------- 1 | extensions = get_loaded_extensions(); 13 | } 14 | 15 | public function extension_loaded($name) 16 | { 17 | // for parent coverage 18 | $this->__call('extension_loaded', array($name)); 19 | 20 | // for testing 21 | return in_array($name, $this->extensions); 22 | } 23 | 24 | public function function_exists($name) 25 | { 26 | if (isset($this->functions[$name])) { 27 | return $this->functions[$name]; 28 | } else { 29 | return $this->__call('function_exists', array($name)); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/FakeSessionHandler.php: -------------------------------------------------------------------------------- 1 | data[$session_id] = null; 17 | return true; 18 | } 19 | 20 | public function gc($maxlifetime) 21 | { 22 | return true; 23 | } 24 | 25 | public function open($save_path, $session_id) 26 | { 27 | return true; 28 | } 29 | 30 | public function read($session_id) 31 | { 32 | return isset($this->data[$session_id]) ? $this->data[$session_id] : ''; 33 | } 34 | 35 | public function write($session_id, $session_data) 36 | { 37 | $this->data[$session_id] = $session_data; 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Issue23Test.php: -------------------------------------------------------------------------------- 1 | session = $this->newSession(); 20 | $this->segment = $this->session->getSegment($this->name); 21 | } 22 | 23 | protected function newSession(array $cookies = array()) 24 | { 25 | // start session earlier 26 | session_start(); 27 | return new Session( 28 | new SegmentFactory, 29 | new CsrfTokenFactory(new Randval()), 30 | new Phpfunc, 31 | $cookies 32 | ); 33 | } 34 | 35 | public function testFlash() 36 | { 37 | // set a value 38 | $this->segment->setFlash('foo', 'bar'); 39 | $expect = 'bar'; 40 | $this->assertSame($expect, $this->segment->getFlashNext('foo')); 41 | $this->assertNull($this->segment->getFlash('foo')); 42 | 43 | // set a value and make it available now 44 | $this->segment->setFlashNow('baz', 'dib'); 45 | $expect = 'dib'; 46 | $this->assertSame($expect, $this->segment->getFlash('baz')); 47 | $this->assertSame($expect, $this->segment->getFlashNext('baz')); 48 | 49 | // clear the next values 50 | $this->segment->clearFlash(); 51 | $this->assertNull($this->segment->getFlashNext('foo')); 52 | $this->assertNull($this->segment->getFlashNext('baz')); 53 | $expect = 'dib'; 54 | $this->assertSame($expect, $this->segment->getFlash('baz')); 55 | 56 | // set some current values and make sure they get kept 57 | $now =& $_SESSION[Session::FLASH_NOW][$this->name]; 58 | $now['foo'] = 'bar'; 59 | $now['baz'] = 'dib'; 60 | $this->segment->keepFlash(); 61 | $this->assertSame('bar', $this->segment->getFlashNext('foo')); 62 | $this->assertSame('dib', $this->segment->getFlashNext('baz')); 63 | 64 | // clear the current and future values 65 | $this->segment->clearFlashNow(); 66 | $this->assertNull($this->segment->getFlash('foo')); 67 | $this->assertNull($this->segment->getFlashNext('foo')); 68 | $this->assertNull($this->segment->getFlash('baz')); 69 | $this->assertNull($this->segment->getFlashNext('baz')); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/SegmentTest.php: -------------------------------------------------------------------------------- 1 | session = $this->newSession(); 20 | $this->segment = $this->session->getSegment($this->name); 21 | } 22 | 23 | protected function newSession(array $cookies = array()) 24 | { 25 | return new Session( 26 | new SegmentFactory, 27 | new CsrfTokenFactory(new Randval()), 28 | new Phpfunc, 29 | $cookies 30 | ); 31 | } 32 | 33 | protected function getValue($key = null) 34 | { 35 | if ($key) { 36 | return $_SESSION[$this->name][$key]; 37 | } else { 38 | return $_SESSION[$this->name]; 39 | } 40 | } 41 | 42 | protected function setValue($key, $val) 43 | { 44 | $_SESSION[$this->name][$key] = $val; 45 | } 46 | 47 | public function testMagicMethods() 48 | { 49 | $this->assertNull($this->segment->get('foo')); 50 | 51 | $this->segment->set('foo', 'bar'); 52 | $this->assertSame('bar', $this->segment->get('foo')); 53 | $this->assertSame('bar', $this->getValue('foo')); 54 | 55 | $this->setValue('foo', 'zim'); 56 | $this->assertSame('zim', $this->segment->get('foo')); 57 | } 58 | 59 | public function testClear() 60 | { 61 | $this->segment->set('foo', 'bar'); 62 | $this->segment->set('baz', 'dib'); 63 | $this->assertSame('bar', $this->getValue('foo')); 64 | $this->assertSame('dib', $this->getValue('baz')); 65 | 66 | // now clear the data 67 | $this->segment->clear(); 68 | $this->assertSame(array(), $this->getValue()); 69 | $this->assertNull($this->segment->get('foo')); 70 | $this->assertNull($this->segment->get('baz')); 71 | } 72 | 73 | public function testGetSegment() 74 | { 75 | $this->segment->set('foo', 'bar'); 76 | $this->segment->set('baz', 'dib'); 77 | $this->assertSame('bar', $this->getValue('foo')); 78 | $this->assertSame('dib', $this->getValue('baz')); 79 | 80 | // now get the data 81 | $this->assertSame(array('foo' => 'bar', 'baz' => 'dib'), $this->segment->getSegment()); 82 | } 83 | 84 | public function testFlash() 85 | { 86 | // set a value 87 | $this->segment->setFlash('foo', 'bar'); 88 | $expect = 'bar'; 89 | $this->assertSame($expect, $this->segment->getFlashNext('foo')); 90 | $this->assertNull($this->segment->getFlash('foo')); 91 | 92 | // set a value and make it available now 93 | $this->segment->setFlashNow('baz', 'dib'); 94 | $expect = 'dib'; 95 | $this->assertSame($expect, $this->segment->getFlash('baz')); 96 | $this->assertSame($expect, $this->segment->getFlashNext('baz')); 97 | 98 | // clear the next values 99 | $this->segment->clearFlash(); 100 | $this->assertNull($this->segment->getFlashNext('foo')); 101 | $this->assertNull($this->segment->getFlashNext('baz')); 102 | $expect = 'dib'; 103 | $this->assertSame($expect, $this->segment->getFlash('baz')); 104 | 105 | // set some current values and make sure they get kept 106 | $now =& $_SESSION[Session::FLASH_NOW][$this->name]; 107 | $now['foo'] = 'bar'; 108 | $now['baz'] = 'dib'; 109 | $this->segment->keepFlash(); 110 | $this->assertSame('bar', $this->segment->getFlashNext('foo')); 111 | $this->assertSame('dib', $this->segment->getFlashNext('baz')); 112 | 113 | // clear the current and future values 114 | $this->segment->clearFlashNow(); 115 | $this->assertNull($this->segment->getFlash('foo')); 116 | $this->assertNull($this->segment->getFlashNext('foo')); 117 | $this->assertNull($this->segment->getFlash('baz')); 118 | $this->assertNull($this->segment->getFlashNext('baz')); 119 | } 120 | 121 | public function testGetDoesNotStartSession() 122 | { 123 | $this->assertFalse($this->session->isStarted()); 124 | $foo = $this->segment->get('foo'); 125 | $this->assertNull($foo); 126 | $this->assertFalse($this->session->isStarted()); 127 | } 128 | 129 | public function testGetResumesSession() 130 | { 131 | // fake a cookie 132 | $cookies = array( 133 | $this->session->getName() => 'fake-cookie-value', 134 | ); 135 | $this->session = $this->newSession($cookies); 136 | 137 | // should be active now, even though not started 138 | $this->assertTrue($this->session->isResumable()); 139 | 140 | // reset the segment to use the new session manager 141 | $this->segment = $this->session->getSegment($this->name); 142 | 143 | // this should restart the session 144 | $foo = $this->segment->get('foo'); 145 | $this->assertTrue($this->session->isStarted()); 146 | } 147 | 148 | public function testSetStartsSessionAndCanReadAfter() 149 | { 150 | // no session yet 151 | $this->assertFalse($this->session->isStarted()); 152 | 153 | // set it 154 | $this->segment->set('foo', 'bar'); 155 | 156 | // session should have started 157 | $this->assertTrue($this->session->isStarted()); 158 | 159 | // get it from the session 160 | $foo = $this->segment->get('foo'); 161 | $this->assertSame('bar', $foo); 162 | 163 | // make sure it's actually in $_SESSION 164 | $this->assertSame($foo, $_SESSION[$this->name]['foo']); 165 | } 166 | 167 | public function testClearDoesNotStartSession() 168 | { 169 | $this->assertFalse($this->session->isStarted()); 170 | $this->segment->clear(); 171 | $this->assertFalse($this->session->isStarted()); 172 | } 173 | 174 | public function testSetFlashStartsSessionAndCanReadAfter() 175 | { 176 | // no session yet 177 | $this->assertFalse($this->session->isStarted()); 178 | 179 | // set it 180 | $this->segment->setFlash('foo', 'bar'); 181 | 182 | // session should have started 183 | $this->assertTrue($this->session->isStarted()); 184 | 185 | // should see it in the session 186 | $actual = $_SESSION[Session::FLASH_NEXT][$this->name]['foo']; 187 | $this->assertSame('bar', $actual); 188 | 189 | } 190 | 191 | public function testGetFlashDoesNotStartSession() 192 | { 193 | $this->assertFalse($this->session->isStarted()); 194 | $this->assertNull($this->segment->getFlash('foo')); 195 | $this->assertFalse($this->session->isStarted()); 196 | } 197 | 198 | public function testRemoveKey(){ 199 | $this->segment->set('foo', 'bar'); 200 | $this->segment->set('baz', 'dib'); 201 | $this->assertSame('bar', $this->getValue('foo')); 202 | $this->assertSame('dib', $this->getValue('baz')); 203 | 204 | // now remove the key 205 | $this->segment->remove('foo'); 206 | $this->assertNull($this->segment->get('foo')); 207 | $this->assertArrayNotHasKey('foo', $_SESSION[$this->name]); 208 | $this->assertSame('dib', $this->getValue('baz')); 209 | } 210 | 211 | public function testRemoveSegment(){ 212 | $this->segment->set('foo', 'bar'); 213 | $this->segment->set('baz', 'dib'); 214 | $this->assertSame('bar', $this->getValue('foo')); 215 | $this->assertSame('dib', $this->getValue('baz')); 216 | 217 | // now remove the key 218 | $this->segment->remove(); 219 | $this->assertArrayNotHasKey($this->name, $_SESSION); 220 | } 221 | 222 | public function testRestartSessionFlashNotMove() 223 | { 224 | $this->assertFalse($this->session->isStarted()); 225 | 226 | // set it 227 | $this->segment->setFlash('foo', 'bar'); 228 | 229 | // session should have started 230 | $this->assertTrue($this->session->isStarted()); 231 | 232 | // should see it in the session 233 | $actual = $_SESSION[Session::FLASH_NEXT][$this->name]['foo']; 234 | $this->assertSame('bar', $actual); 235 | 236 | $this->session->commit(); 237 | 238 | $this->assertFalse($this->session->isStarted()); 239 | 240 | $this->session->start(); 241 | 242 | // should see it in the session 243 | $actual = $_SESSION[Session::FLASH_NEXT][$this->name]['foo']; 244 | $this->assertSame('bar', $actual); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /tests/SessionFactoryTest.php: -------------------------------------------------------------------------------- 1 | newInstance($_COOKIE); 12 | $this->assertInstanceOf('Aura\Session\Session', $session); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/SessionTest.php: -------------------------------------------------------------------------------- 1 | phpfunc = new FakePhpfunc; 21 | $handler = new FakeSessionHandler(); 22 | session_set_save_handler( 23 | array($handler, 'open'), 24 | array($handler, 'close'), 25 | array($handler, 'read'), 26 | array($handler, 'write'), 27 | array($handler, 'destroy'), 28 | array($handler, 'gc') 29 | ); 30 | $this->session = $this->newSession(); 31 | } 32 | 33 | protected function newSession(array $cookies = array()) 34 | { 35 | return new Session( 36 | new SegmentFactory, 37 | new CsrfTokenFactory(new Randval()), 38 | $this->phpfunc, 39 | $cookies 40 | ); 41 | } 42 | 43 | public function teardown(): void 44 | { 45 | session_unset(); 46 | if (session_id() !== '') { 47 | session_destroy(); 48 | } 49 | } 50 | 51 | public function testStart() 52 | { 53 | $this->session->start(); 54 | $this->assertTrue($this->session->isStarted()); 55 | } 56 | 57 | public function testClear() 58 | { 59 | // get a test segment and set some data 60 | $segment = $this->session->getSegment('test'); 61 | $segment->set('foo', 'bar'); 62 | $segment->set('baz', 'dib'); 63 | 64 | $expect = array( 65 | Session::FLASH_NEXT => array( 66 | 'test' => array(), 67 | ), 68 | Session::FLASH_NOW => array( 69 | 'test' => array(), 70 | ), 71 | 'test' => array( 72 | 'foo' => 'bar', 73 | 'baz' => 'dib', 74 | ), 75 | ); 76 | 77 | $this->assertSame($expect, $_SESSION); 78 | 79 | // now clear it 80 | $this->session->clear(); 81 | $this->assertSame(array(), $_SESSION); 82 | } 83 | 84 | public function testDestroy() 85 | { 86 | // get a test segment and set some data 87 | $segment = $this->session->getSegment('test'); 88 | $segment->set('foo', 'bar'); 89 | $segment->set('baz', 'dib'); 90 | 91 | $this->assertTrue($this->session->isStarted()); 92 | 93 | $expect = array( 94 | Session::FLASH_NEXT => array( 95 | 'test' => array(), 96 | ), 97 | Session::FLASH_NOW => array( 98 | 'test' => array(), 99 | ), 100 | 'test' => array( 101 | 'foo' => 'bar', 102 | 'baz' => 'dib', 103 | ), 104 | ); 105 | 106 | $this->assertSame($expect, $_SESSION); 107 | 108 | // now destroy it 109 | $this->session->destroy(); 110 | $this->assertFalse($this->session->isStarted()); 111 | } 112 | 113 | public function testCommit() 114 | { 115 | $this->session->commit(); 116 | $this->assertFalse($this->session->isStarted()); 117 | } 118 | 119 | public function testCommitAndDestroy() 120 | { 121 | // get a test segment and set some data 122 | $segment = $this->session->getSegment('test'); 123 | $segment->set('foo', 'bar'); 124 | $segment->set('baz', 'dib'); 125 | 126 | $this->assertTrue($this->session->isStarted()); 127 | 128 | $expect = array( 129 | Session::FLASH_NEXT => array( 130 | 'test' => array(), 131 | ), 132 | Session::FLASH_NOW => array( 133 | 'test' => array(), 134 | ), 135 | 'test' => array( 136 | 'foo' => 'bar', 137 | 'baz' => 'dib', 138 | ), 139 | ); 140 | 141 | $this->assertSame($expect, $_SESSION); 142 | 143 | $this->session->commit(); 144 | $this->session->destroy(); 145 | $segment = $this->session->getSegment('test'); 146 | $this->assertSame(array(), $_SESSION); 147 | } 148 | 149 | public function testGetSegment() 150 | { 151 | $segment = $this->session->getSegment('test'); 152 | $this->assertInstanceof('Aura\Session\Segment', $segment); 153 | } 154 | 155 | public function testGetCsrfToken() 156 | { 157 | $actual = $this->session->getCsrfToken(); 158 | $expect = 'Aura\Session\CsrfToken'; 159 | $this->assertInstanceOf($expect, $actual); 160 | } 161 | 162 | public function testisResumable() 163 | { 164 | // should not look active 165 | $this->assertFalse($this->session->isResumable()); 166 | 167 | // fake a cookie 168 | $cookies = array( 169 | $this->session->getName() => 'fake-cookie-value', 170 | ); 171 | $this->session = $this->newSession($cookies); 172 | 173 | // now it should look active 174 | $this->assertTrue($this->session->isResumable()); 175 | } 176 | 177 | public function testGetAndRegenerateId() 178 | { 179 | $this->session->start(); 180 | $old_id = $this->session->getId(); 181 | $this->session->regenerateId(); 182 | $new_id = $this->session->getId(); 183 | $this->assertTrue($old_id != $new_id); 184 | 185 | // check the csrf token as well 186 | $old_value = $this->session->getCsrfToken()->getValue(); 187 | $this->session->regenerateId(); 188 | $new_value = $this->session->getCsrfToken()->getValue(); 189 | $this->assertNotEquals($old_value, $new_value); 190 | 191 | // Regenerates multiple keys 192 | $csrf1_old_value = $this->session->getCsrfToken('csrf1')->getValue(); 193 | $csrf2_old_value = $this->session->getCsrfToken('csrf2')->getValue(); 194 | $this->session->regenerateId(); 195 | $csrf1_new_value = $this->session->getCsrfToken('csrf1')->getValue(); 196 | $csrf2_new_value = $this->session->getCsrfToken('csrf2')->getValue(); 197 | $this->assertNotSame($csrf1_old_value, $csrf1_new_value); 198 | $this->assertNotSame($csrf2_old_value, $csrf2_new_value); 199 | } 200 | 201 | public function testSetAndGetName() 202 | { 203 | $expect = 'new_name'; 204 | $this->session->setName($expect); 205 | $actual = $this->session->getName(); 206 | $this->assertSame($expect, $actual); 207 | } 208 | 209 | public function testSetAndGetSavePath() 210 | { 211 | $expect = '/new/save/path'; 212 | $this->session->setSavePath($expect); 213 | $actual = $this->session->getSavePath(); 214 | $this->assertSame($expect, $actual); 215 | } 216 | 217 | public function testSetAndGetCookieParams() 218 | { 219 | $expect = $this->session->getCookieParams(); 220 | $expect['lifetime'] = '999'; 221 | $this->session->setCookieParams($expect); 222 | $actual = $this->session->getCookieParams(); 223 | $this->assertSame($expect, $actual); 224 | 225 | // Cannot change session cookie parameters when session is active 226 | $this->expectException(SessionAlreadyStarted::class); 227 | $this->session->start(); 228 | $newParams = $expect; 229 | $newParams['lifetime'] = '0'; 230 | $this->session->setCookieParams($newParams); 231 | } 232 | 233 | public function testSetAndGetCacheExpire() 234 | { 235 | $expect = 123; 236 | $this->session->setCacheExpire($expect); 237 | $actual = $this->session->getCacheExpire(); 238 | $this->assertSame($expect, $actual); 239 | } 240 | 241 | public function testSetAndGetCacheLimiter() 242 | { 243 | $expect = 'private_no_cache'; 244 | $this->session->setCacheLimiter($expect); 245 | $actual = $this->session->getCacheLimiter(); 246 | $this->assertSame($expect, $actual); 247 | } 248 | 249 | public function testResume() 250 | { 251 | // should not look active 252 | $this->assertFalse($this->session->isResumable()); 253 | $this->assertFalse($this->session->resume()); 254 | 255 | // fake a cookie so a session looks available 256 | $cookies = array( 257 | $this->session->getName() => 'fake-cookie-value', 258 | ); 259 | $this->session = $this->newSession($cookies); 260 | $this->assertTrue($this->session->resume()); 261 | 262 | // now it should already active 263 | $this->assertTrue($this->session->resume()); 264 | } 265 | 266 | public function testIsStarted_php53() 267 | { 268 | $this->phpfunc->functions = array('session_status' => false); 269 | $this->session = $this->newSession(); 270 | $this->assertFalse($this->session->isStarted()); 271 | $this->session->start(); 272 | $this->assertTrue($this->session->isStarted()); 273 | } 274 | } 275 | --------------------------------------------------------------------------------