├── tests ├── EntropyGenerator │ ├── files │ │ ├── empty.txt │ │ └── urandom.txt │ ├── UniqTest.php │ └── FileTest.php ├── bootstrap.php ├── SessionFlashTest.php ├── TestCase.php └── SessionTest.php ├── .gitignore ├── src ├── Exception.php ├── Event │ ├── Expired.php │ ├── NoDataOrExpired.php │ ├── InvalidFingerprint.php │ └── Event.php ├── EntropyGenerator │ ├── EntropyGeneratorInterface.php │ ├── Uniq.php │ └── File.php ├── FingerprintGenerator │ ├── FingerprintGeneratorInterface.php │ └── UserAgent.php ├── Store │ ├── StoreInterface.php │ ├── DoctrineCache.php │ └── FileStore.php ├── Id │ ├── Store │ │ ├── StoreInterface.php │ │ └── Cookie.php │ └── Handler.php ├── User │ └── Session.php ├── SessionFlash.php └── Session.php ├── .travis.yml ├── .scrutinizer.yml ├── sesshin ├── phpunit.xml ├── CHANGELOG ├── composer.json ├── LICENSE ├── composer.lock ├── .phpunit.result.cache └── README.md /tests/EntropyGenerator/files/empty.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/coverage 2 | tests/testdox 3 | vendor 4 | .idea 5 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | assertRegExp('/\w+\.\w+/', $uniqGenerator->generate()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/FingerprintGenerator/FingerprintGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Id/Store/StoreInterface.php: -------------------------------------------------------------------------------- 1 | session = $session; 20 | } 21 | 22 | /** 23 | * @return Session 24 | */ 25 | public function getSession() 26 | { 27 | return $this->session; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/EntropyGenerator/files/urandom.txt: -------------------------------------------------------------------------------- 1 | ?̘۴??q??M?A0??) 2 | |??)"?Т??d?j}>?P????g%\????BV??jhX?????_?U?|???0??B????Qܡ?(??7?i?fzp???2"']??\ 3 | _+`?{8ߞ߶E#??W^?r+??ۘ?f?v??~??6?P????g%\????BV??jhX?????_?U?|???0??B????Qܡ?(??7?i?fzp???2"']??\ 9 | _+`?{8ߞ߶E#??W^?r+??ۘ?f?v??~??6userAgent = $userAgent; 19 | } 20 | 21 | /** 22 | * @return string 23 | */ 24 | public function generate() 25 | { 26 | return sha1($this->userAgent); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/EntropyGenerator/File.php: -------------------------------------------------------------------------------- 1 | file = $file; 18 | $this->length = $length; 19 | } 20 | 21 | /** 22 | * @return string 23 | * @throws Exception 24 | */ 25 | public function generate() 26 | { 27 | $entropy = file_get_contents($this->file, false, null, 0, $this->length); 28 | if (empty($entropy)) { 29 | throw new Exception('Entropy file is empty.'); 30 | } 31 | 32 | return $entropy; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "league/sesshin", 3 | "description": "PHP secure advanced session manager", 4 | "keywords": ["session"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Przemek Sobstel", 10 | "email": "przemek@sobstel.org", 11 | "homepage": "http://sobstel.org" 12 | } 13 | ], 14 | "support": { 15 | "email": "przemek@sobstel.org" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Sesshin\\Tests\\": "tests/", 20 | "Sesshin\\": "src/" 21 | } 22 | }, 23 | "require": { 24 | "php": ">=5.4.0", 25 | "league/event": "~2.0" 26 | }, 27 | "suggest": { 28 | "doctrine/cache": "To use one of numerous doctrine cache drivers for storing" 29 | }, 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "1.2.x-dev" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/EntropyGenerator/FileTest.php: -------------------------------------------------------------------------------- 1 | generate(); 12 | 13 | $this->assertEquals(strlen($entropy), 512); 14 | } 15 | 16 | public function testBytesReadFromFileCanBeSpecified() 17 | { 18 | $urandomGenerator = new File(__DIR__.'/files/urandom.txt', 64); 19 | $entropy = $urandomGenerator->generate(); 20 | 21 | $this->assertEquals(strlen($entropy), 64); 22 | } 23 | 24 | public function testThrowsExceptionOnEmptyFile() 25 | { 26 | $this->expectException(\Sesshin\Exception::class); 27 | $fileGenerator = new File(__DIR__.'/files/empty.txt'); 28 | $fileGenerator->generate(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/SessionFlashTest.php: -------------------------------------------------------------------------------- 1 | temp_dir = __DIR__.'/temp'; 17 | $this->session = new Session(new FileStore($this->temp_dir)); 18 | } 19 | 20 | 21 | function testSavingAndObtainingDataFromFlash() 22 | { 23 | $this->session->flash()->set('key1','Hello Hello1'); 24 | $this->session->flash()->set('key2','Hello Hello2'); 25 | 26 | $this->assertEquals('Hello Hello1',$this->session->flash()->get('key1')); 27 | $this->assertEquals('Hello Hello2',$this->session->flash()->get('key2')); 28 | } 29 | 30 | 31 | function testMovementFromNewDataToOldData(){ 32 | $this->assertEmpty($this->session->flash()->get('key1')); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2014 Przemek Sobstel (przemek@sobstel.org, http://sobstel.org/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/User/Session.php: -------------------------------------------------------------------------------- 1 | userIdKey = $userIdKey; 16 | } 17 | 18 | /** 19 | * @return string 20 | */ 21 | public function getUserIdKey() 22 | { 23 | return $this->userIdKey; 24 | } 25 | 26 | /** 27 | * @return mixed 28 | */ 29 | public function getUserId() 30 | { 31 | return $this->getValue($this->getUserIdKey()); 32 | } 33 | 34 | /** 35 | * @param string $userId 36 | */ 37 | public function login($userId) 38 | { 39 | $this->setValue($this->getUserIdKey(), $userId); 40 | } 41 | 42 | /** 43 | * @return bool 44 | */ 45 | public function isLogged() 46 | { 47 | return !is_null($this->getUserId()); 48 | } 49 | 50 | /** 51 | */ 52 | public function logout() 53 | { 54 | $this->unsetValue($this->getUserIdKey()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | setAccessible(true); 13 | 14 | return $ref_prop; 15 | } 16 | 17 | /** 18 | * @return \ReflectionProperty 19 | */ 20 | public function setPropertyValue($object, $propertyName, $value) 21 | { 22 | $ref_prop = $this->setPropertyAccessible($object, $propertyName); 23 | $ref_prop->setValue($object, $value); 24 | 25 | return $ref_prop; 26 | } 27 | 28 | public function setMethodAccessible($object, $methodName) 29 | { 30 | $ref_method = new \ReflectionMethod($object, $methodName); 31 | $ref_method->setAccessible(true); 32 | 33 | return $ref_method; 34 | } 35 | 36 | public function invokeMethod($object, $methodName, $args = array()) 37 | { 38 | $ref_method = $this->setMethodAccessible($object, $methodName); 39 | 40 | return $ref_method->invokeArgs($object, $args); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Store/DoctrineCache.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 20 | } 21 | 22 | /** 23 | * @param string $id 24 | * @return bool|mixed 25 | */ 26 | public function fetch($id) 27 | { 28 | return $this->cache->fetch($id); 29 | } 30 | 31 | /** 32 | * @param string $id 33 | * @param mixed $data 34 | * @param int $lifeTime 35 | * @return bool 36 | */ 37 | public function save($id, $data, $lifeTime) 38 | { 39 | return $this->cache->save($id, $data, $lifeTime); 40 | } 41 | 42 | /** 43 | * @param string $id 44 | * @return bool 45 | */ 46 | public function delete($id) 47 | { 48 | return $this->cache->delete($id); 49 | } 50 | 51 | /** 52 | * @return Cache 53 | */ 54 | public function getCache() 55 | { 56 | return $this->cache; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Store/FileStore.php: -------------------------------------------------------------------------------- 1 | dir = $dir; 15 | } 16 | 17 | /** 18 | * @param string $id 19 | * @return bool|mixed 20 | */ 21 | public function fetch($id) 22 | { 23 | $fileName = $this->getFileName($id); 24 | 25 | if (file_exists($fileName)) { 26 | list($expirationTime, $content) = explode('|', file_get_contents($fileName)); 27 | 28 | if ($expirationTime < time()) { 29 | $this->delete($id); 30 | 31 | return false; 32 | } 33 | 34 | return unserialize($content); 35 | } 36 | 37 | return false; 38 | } 39 | 40 | /** 41 | * @param string $id 42 | * @param mixed $data 43 | * @param int $lifeTime 44 | * @return int 45 | */ 46 | public function save($id, $data, $lifeTime) 47 | { 48 | $fileName = $this->getFileName($id); 49 | 50 | $expirationTime = time() + $lifeTime; 51 | $content = $expirationTime . '|' . serialize($data); 52 | 53 | return file_put_contents($fileName, $content); 54 | } 55 | 56 | /** 57 | * @param string $id 58 | * @return bool 59 | */ 60 | public function delete($id) 61 | { 62 | return unlink($this->getFileName($id)); 63 | } 64 | 65 | /** 66 | * @param string $id 67 | * @return string 68 | */ 69 | protected function getFileName($id) 70 | { 71 | return $this->dir . DIRECTORY_SEPARATOR . $id . '.sess'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Id/Store/Cookie.php: -------------------------------------------------------------------------------- 1 | name = $name; 36 | $this->path = $path; 37 | $this->domain = $domain; 38 | $this->secure = $secure; 39 | $this->httpOnly = $httpOnly; 40 | } 41 | 42 | /** 43 | * @param string $id 44 | */ 45 | public function setId($id) 46 | { 47 | if (setcookie($this->name, $id, 0, $this->path, $this->domain, $this->secure, $this->httpOnly)) { 48 | $this->id = $id; 49 | } 50 | } 51 | 52 | /** 53 | * @return string 54 | * @throws Exception 55 | */ 56 | public function getId() 57 | { 58 | if ($this->issetId()) { 59 | return isset($this->id) ? $this->id : $_COOKIE[$this->name]; 60 | } 61 | throw new Exception('Id is not set'); 62 | } 63 | 64 | /** 65 | * @return bool 66 | */ 67 | public function issetId() 68 | { 69 | return (isset($this->id) || isset($_COOKIE[$this->name])); 70 | } 71 | 72 | /** 73 | * @return void 74 | */ 75 | public function unsetId() 76 | { 77 | setcookie($this->name, '', 1, $this->path, $this->domain, $this->secure, $this->httpOnly); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "9cbb197d09dfd15afa244f40000beeae", 8 | "packages": [ 9 | { 10 | "name": "league/event", 11 | "version": "2.2.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/thephpleague/event.git", 15 | "reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/thephpleague/event/zipball/d2cc124cf9a3fab2bb4ff963307f60361ce4d119", 20 | "reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.4.0" 25 | }, 26 | "require-dev": { 27 | "henrikbjorn/phpspec-code-coverage": "~1.0.1", 28 | "phpspec/phpspec": "^2.2" 29 | }, 30 | "type": "library", 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "2.2-dev" 34 | } 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "League\\Event\\": "src/" 39 | } 40 | }, 41 | "notification-url": "https://packagist.org/downloads/", 42 | "license": [ 43 | "MIT" 44 | ], 45 | "authors": [ 46 | { 47 | "name": "Frank de Jonge", 48 | "email": "info@frenky.net" 49 | } 50 | ], 51 | "description": "Event package", 52 | "keywords": [ 53 | "emitter", 54 | "event", 55 | "listener" 56 | ], 57 | "time": "2018-11-26T11:52:41+00:00" 58 | } 59 | ], 60 | "packages-dev": [], 61 | "aliases": [], 62 | "minimum-stability": "stable", 63 | "stability-flags": [], 64 | "prefer-stable": false, 65 | "prefer-lowest": false, 66 | "platform": { 67 | "php": ">=5.4.0" 68 | }, 69 | "platform-dev": [] 70 | } 71 | -------------------------------------------------------------------------------- /src/Id/Handler.php: -------------------------------------------------------------------------------- 1 | idStore = $idStore; 30 | } 31 | 32 | /** 33 | * @return StoreInterface 34 | */ 35 | public function getIdStore() 36 | { 37 | if (!$this->idStore) { 38 | $this->idStore = new CookieStore(); 39 | } 40 | 41 | return $this->idStore; 42 | } 43 | 44 | /** 45 | * Sets entropy that is used to generate session id. 46 | * 47 | * @param EntropyGeneratorInterface $entropyGenerator 48 | */ 49 | public function setEntropyGenerator(EntropyGeneratorInterface $entropyGenerator) 50 | { 51 | $this->entropyGenerator = $entropyGenerator; 52 | } 53 | 54 | /** 55 | * @return EntropyGeneratorInterface 56 | */ 57 | public function getEntropyGenerator() 58 | { 59 | if (!$this->entropyGenerator) { 60 | $this->entropyGenerator = new Uniq(); 61 | } 62 | 63 | return $this->entropyGenerator; 64 | } 65 | 66 | /** 67 | * @param string $algo Hash algorith accepted by hash extension. 68 | * @throws Exception 69 | */ 70 | public function setHashAlgo($algo) 71 | { 72 | if (in_array($algo, hash_algos())) { 73 | $this->hashAlgo = $algo; 74 | } else { 75 | throw new Exception('Provided algo is not valid (not on hash_algos() list)'); 76 | } 77 | } 78 | 79 | /** 80 | * @return string 81 | */ 82 | public function getHashAlgo() 83 | { 84 | return $this->hashAlgo; 85 | } 86 | 87 | /** 88 | * @return string 89 | */ 90 | public function generateId() 91 | { 92 | $id = hash($this->getHashAlgo(), $this->getEntropyGenerator()->generate()); 93 | $this->setId($id); 94 | 95 | return $this->getId(); 96 | } 97 | 98 | /** 99 | * @param string $id 100 | */ 101 | public function setId($id) 102 | { 103 | $this->getIdStore()->setId($id); 104 | } 105 | 106 | /** 107 | * @return string 108 | */ 109 | public function getId() 110 | { 111 | return $this->getIdStore()->getId(); 112 | } 113 | 114 | /** 115 | * @return bool 116 | */ 117 | public function issetId() 118 | { 119 | return $this->getIdStore()->issetId(); 120 | } 121 | 122 | /** 123 | * @return void 124 | */ 125 | public function unsetId() 126 | { 127 | $this->getIdStore()->unsetId(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | C:37:"PHPUnit\Runner\DefaultTestResultCache":4118:{a:2:{s:7:"defects";a:2:{s:69:"Sesshin\Tests\EntropyGenerator\FileTest::testUsesUrandomFileByDefault";i:6;s:71:"Sesshin\Tests\EntropyGenerator\FileTest::testThrowsExceptionOnEmptyFile";i:6;}s:5:"times";a:49:{s:69:"Sesshin\Tests\EntropyGenerator\FileTest::testUsesUrandomFileByDefault";d:0;s:75:"Sesshin\Tests\EntropyGenerator\FileTest::testReads512BytesFromFileByDefault";d:0;s:76:"Sesshin\Tests\EntropyGenerator\FileTest::testBytesReadFromFileCanBeSpecified";d:0;s:71:"Sesshin\Tests\EntropyGenerator\FileTest::testThrowsExceptionOnEmptyFile";d:0;s:62:"Sesshin\Tests\EntropyGenerator\UniqTest::testGeneratesUniqueId";d:0;s:67:"Sesshin\Tests\SessionFlashTest::testSavingAndObtainingDataFromFlash";d:0.001;s:64:"Sesshin\Tests\SessionFlashTest::testMovementFromNewDataToOldData";d:0;s:68:"Sesshin\Tests\SessionTest::testValueIsSetToDefaultNamespaceByDefault";d:0.001;s:59:"Sesshin\Tests\SessionTest::testCanSetValueToCustomNamespace";d:0;s:42:"Sesshin\Tests\SessionTest::testCanGetValue";d:0;s:80:"Sesshin\Tests\SessionTest::testCanGetValueMethodReturnsNullIfNoValueForGivenName";d:0;s:51:"Sesshin\Tests\SessionTest::testCanCheckIfValueIsSet";d:0;s:45:"Sesshin\Tests\SessionTest::testCanUnsetValues";d:0;s:50:"Sesshin\Tests\SessionTest::testCanGetAndUnsetValue";d:0;s:58:"Sesshin\Tests\SessionTest::testCanGetAllValuesForNamespace";d:0;s:60:"Sesshin\Tests\SessionTest::testCanUnsetAllValuesForNamespace";d:0;s:50:"Sesshin\Tests\SessionTest::testCanGetRequestsCount";d:0;s:54:"Sesshin\Tests\SessionTest::testCreateMethodGeneratesId";d:0;s:58:"Sesshin\Tests\SessionTest::testCreateMethodUnsetsAllValues";d:0;s:59:"Sesshin\Tests\SessionTest::testCreateMethodResetsFirstTrace";d:0;s:58:"Sesshin\Tests\SessionTest::testCreateMethodResetsLastTrace";d:0;s:62:"Sesshin\Tests\SessionTest::testCreateMethodResetsRequestsCount";d:0;s:68:"Sesshin\Tests\SessionTest::testCreateMethodResetsIdRegenerationTrace";d:0;s:63:"Sesshin\Tests\SessionTest::testCreateMethodGeneratesFingerprint";d:0;s:55:"Sesshin\Tests\SessionTest::testCreateMethodOpensSession";d:0;s:105:"Sesshin\Tests\SessionTest::testOpenMethodWhenCalledWithTrueThenCreatesNewSessionIfSessionNotExistsAlready";d:0;s:110:"Sesshin\Tests\SessionTest::testOpenMethodWhenCalledWithTrueThenDoesNotCreateNewSessionIfSessionIdExistsAlready";d:0.001;s:87:"Sesshin\Tests\SessionTest::testOpenMethodWhenCalledWithFalseThenDoesNotCreateNewSession";d:0.001;s:72:"Sesshin\Tests\SessionTest::testOpenMethodLoadsSessionDataIfSessionExists";d:0;s:81:"Sesshin\Tests\SessionTest::testOpenMethodDoesNotLoadSessionDataIfSessionNotExists";d:0;s:100:"Sesshin\Tests\SessionTest::testOpenMethodTriggersSessionNoDataOrExpiredEventIfNoDataPresentAfterLoad";d:0.001;s:84:"Sesshin\Tests\SessionTest::testOpenMethodTriggersSessionExpiredEventIfSessionExpired";d:0.001;s:98:"Sesshin\Tests\SessionTest::testOpenMethodTriggersInvalidFingerprintEventIfLoadedFingerprintInvalid";d:0.001;s:80:"Sesshin\Tests\SessionTest::testOpenMethodOpenSessionAndIncrementsRequestsCounter";d:0;s:56:"Sesshin\Tests\SessionTest::testCanCheckIfSessionIsOpened";d:0;s:49:"Sesshin\Tests\SessionTest::testCanSetGetIdHandler";d:0;s:59:"Sesshin\Tests\SessionTest::testUsesDefaultIdHandlerIfNotSet";d:0.001;s:83:"Sesshin\Tests\SessionTest::testSessionIdShouldBeRegeneratedIfIdRequestsLimitReached";d:0;s:78:"Sesshin\Tests\SessionTest::testSessionIdShouldBeRegeneratedIfIdTtlLimitReached";d:0;s:80:"Sesshin\Tests\SessionTest::testSessionIdShouldNotBeRegeneratedIfLimitsNotReached";d:0;s:76:"Sesshin\Tests\SessionTest::testSessionIdShouldNotBeRegeneratedIfLimitsNotSet";d:0;s:47:"Sesshin\Tests\SessionTest::testCanSetGetEmitter";d:0;s:57:"Sesshin\Tests\SessionTest::testUsesDefaultEmitterIfNotSet";d:0;s:68:"Sesshin\Tests\SessionTest::testImplementsArrayAccessForSessionValues";d:0.001;s:61:"Sesshin\Tests\SessionTest::testLoadMethodFetchesDataFromStore";d:0;s:68:"Sesshin\Tests\SessionTest::testLoadMethodReturnsFalseIfNoDataInStore";d:0;s:54:"Sesshin\Tests\SessionTest::testPutMethodDataForSession";d:0;s:53:"Sesshin\Tests\SessionTest::testPushMethodForArrayData";d:0;s:52:"Sesshin\Tests\SessionTest::testForgetDataFromSession";d:0;}}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sesshin 2 | 3 | Object-oriented, extendable advanced session handling component written with 4 | security in mind that mitigates attacks like Session Hijacking, Session Fixation, 5 | Session Exposure, Sesion Poisoning, Session Prediction. 6 | 7 | Awarded 1st place in 8 | [php.pl contest](http://wortal.php.pl/phppl/Wortal/Spolecznosc/Konkursy/Konkurs-Pozyteczne-i-praktyczne-biblioteki-Wyniki). 9 | 10 | Features: 11 | 12 | - smart session expiry control 13 | - prevents session adoption, i.e. session ids generated only by the component 14 | are acceptable (strict model) 15 | - sends cookie only when session really created 16 | - session id rotation (anti session hijacking), based on time and/or number of 17 | requests 18 | - configurable: 19 | - unlike PHP native mechanism, you don't have to use cron or resource-consuming 20 | 100% garbage collecting probability to ensure sessions are removed exactly 21 | after specified time 22 | - convention over configuration - possible to configure user-defined stores, listeners (observers), 23 | entropy callback and fingerprint generators, but all of them have defaults set out-of-the-box 24 | - 100% independent from insecure native PHP session extension 25 | 26 | [![Build Status](https://travis-ci.org/sobstel/sesshin.png?branch=master)](https://travis-ci.org/sobstel/sesshin) 27 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/sobstel/sesshin/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/sobstel/sesshin/?branch=master) 28 | 29 | ## Usage 30 | 31 | ### Installation 32 | 33 | ``` 34 | composer require sobstel/sesshin 35 | ``` 36 | 37 | ### Create new session 38 | 39 | Only when `create()` called, session cookie is created (for native PHP session 40 | handler cookie is present all the time whether it's needed or not). 41 | 42 | ```php 43 | $session->create(); 44 | ``` 45 | 46 | ### Open existing session 47 | 48 | If session was not created earlier, session is not opened and `false` is returned. 49 | 50 | ```php 51 | $session->open(); 52 | ``` 53 | 54 | If you want to create new session if it does not exist already, just pass `true` 55 | as argument. It will call `create()` transparently. 56 | 57 | ```php 58 | $session->open(true); 59 | ``` 60 | 61 | ### Regenerate session id 62 | 63 | ```php 64 | // auto-regenerate after specified time (secs) 65 | $session->setIdTtl(300); 66 | 67 | // auto-regenerate after specified number of requests 68 | $session->setIdRequestsLimit(10); 69 | 70 | // manually 71 | $session->regenerateId(); 72 | ``` 73 | 74 | ### Listen to special events 75 | 76 | ```php 77 | use Sesshin\Event\Event; 78 | 79 | $eventEmitter = $session->geEmitter(); 80 | 81 | $eventEmitter->addListener('sesshin.no_data_or_expired', function(Event $event) { 82 | die('Session expired or session adoption attack!'); 83 | }); 84 | $eventEmitter->addListener('sesshin.expired', function(Event $event) { 85 | die(sprintf('Session %s expired!', $event->getSession()->getId())); 86 | }); 87 | $eventEmitter->addListener('sesshin.invalid_fingerprint', function(Event $event) { 88 | die('Invalid fingerprint, possible attack!'); 89 | }); 90 | ``` 91 | 92 | ### User session 93 | 94 | ```php 95 | use Sesshin\User\Session as UserSession; 96 | use Sesshin\Store\FileStore; 97 | 98 | $userSession = new UserSession(new FileStore('/path/to/dir')); 99 | 100 | $userSession->create(); 101 | $userSession->login(123); 102 | 103 | if ($userSession->isLogged()) { 104 | echo sprintf('User %s is logged', $userSession->getUserId()); 105 | 106 | // Or if you have some kind of UserRepository class, which can be used to fetch user data 107 | $user = UserRepository::find($userSession->getUserId()); 108 | echo sprintf('User %s is logged', $user->getUsername()); 109 | } 110 | ``` 111 | 112 | ### Store 113 | 114 | Sesshin provides default FileStore. 115 | 116 | ```php 117 | use Sesshin\Session; 118 | use Sesshin\Store\FileStore; 119 | 120 | $session = new Session(new FileStore('/path/to/dir')); 121 | ``` 122 | 123 | Note! Using /tmp as a directory is not secure on shared hosting. 124 | 125 | Alternatively you can use one of numerous 126 | [doctrine/cache](https://github.com/doctrine/cache/tree/master/lib/Doctrine/Common/Cache) 127 | providers. 128 | 129 | ```php 130 | use Sesshin\Store\DoctrineCache; 131 | use Doctrine\Common\Cache\MemcachedCache; 132 | 133 | $memcached = new Memcached; 134 | // here configure memcached (add servers etc) 135 | 136 | $session = new Session(new DoctrineCache(new MemcachedCache($memcached))); 137 | ``` 138 | 139 | You can also implement your own store using `Sesshin\Store\StoreInterface`. 140 | 141 | ### Change entropy algorithm 142 | 143 | Entropy is used to generate session id. 144 | 145 | ```php 146 | $session->getIdHandler()->setEntropyGenerator(new MyFancyEntropyGenerator()); 147 | ``` 148 | 149 | `MyFancyEntropyGenerator` must implement `Sesshin\EntropyGenerator\EntropyGeneratorInterface`. 150 | 151 | ### Change session ID store 152 | 153 | By default session ID is stored in cookie, but sometimes you may need to force 154 | session id, eg. based on some token, query string var, etc. 155 | 156 | ```php 157 | $session->getIdHandler()->setIdStore(new MyFancyIdStore()); 158 | ``` 159 | 160 | `MyFancyIdStore` must implement `Sesshin\Id\Store\StoreInterface`. 161 | -------------------------------------------------------------------------------- /src/SessionFlash.php: -------------------------------------------------------------------------------- 1 | session = $session; 45 | } 46 | 47 | /** 48 | * Add a value in the flash session. 49 | * 50 | * @param $value 51 | * @param string $type 52 | */ 53 | public function add($value, $type = 'n') 54 | { 55 | $this->session->put($type, $value); 56 | $this->session->push($this->key_name.'.new', $type); 57 | $this->removeFromOldFlashData([$type]); 58 | } 59 | 60 | 61 | /** 62 | * Add a value in the flash session. 63 | * 64 | * @param $key 65 | * @param $value 66 | */ 67 | public function set($key, $value) 68 | { 69 | $this->session->put($key, $value); 70 | $this->session->push($this->key_name.'.new', $key); 71 | $this->removeFromOldFlashData([$key]); 72 | } 73 | 74 | /** 75 | * Get a value in the flash session. 76 | * 77 | * @param $key 78 | * @return mixed 79 | */ 80 | public function get($key) 81 | { 82 | return $this->session->getValue($key); 83 | } 84 | 85 | /** 86 | * Determine if exist key in flash data. 87 | * 88 | * @param $key 89 | * @return bool 90 | */ 91 | public function has($key) 92 | { 93 | $current_data = $this->session->getValue($key); 94 | 95 | return isset($current_data); 96 | } 97 | 98 | /** 99 | * Get all the data or data of a type. 100 | * 101 | * @return mixed 102 | */ 103 | public function getCurrentData() 104 | { 105 | $current_data = $this->session->getValue($this->key_name); 106 | 107 | return isset($current_data) ? $current_data : $current_data = array(); 108 | } 109 | 110 | /** 111 | * Reflash all of the session flash data. 112 | * 113 | * @return void 114 | */ 115 | public function reflash() 116 | { 117 | $this->mergeNewFlashes($this->session->getValue($this->key_name.'.old')); 118 | $this->session->setValue($this->key_name.'.old', []); 119 | } 120 | 121 | 122 | /** 123 | * Reflash a subset of the current flash data. 124 | * 125 | * @param array|mixed $keys 126 | * @return void 127 | */ 128 | public function keep($keys = null) 129 | { 130 | $this->mergeNewFlashes($keys = is_array($keys) ? $keys : func_get_args()); 131 | $this->removeFromOldFlashData($keys); 132 | } 133 | 134 | 135 | /** 136 | * Merge new flash keys into the new flash array. 137 | * 138 | * @param array $keys 139 | * @return void 140 | */ 141 | protected function mergeNewFlashes(array $keys) 142 | { 143 | $values = array_unique(array_merge($this->session->getValue($this->key_name.'.new'), $keys)); 144 | $this->session->setValue($this->key_name.'.new', $values); 145 | } 146 | 147 | /** 148 | * Remove the given keys from the old flash data. 149 | * 150 | * @param array $keys 151 | * @return void 152 | */ 153 | protected function removeFromOldFlashData(array $keys) 154 | { 155 | $old_data = $this->session->getValue($this->key_name.'.old'); 156 | 157 | if (! is_array($old_data)) { 158 | $old_data = array(); 159 | } 160 | $this->session->setValue($this->key_name.'.old', array_diff($old_data, $keys)); 161 | } 162 | 163 | /** 164 | * Age the flash data for the session. 165 | * 166 | * @return void 167 | */ 168 | public function ageFlashData() 169 | { 170 | $old_data = $this->session->getValue($this->key_name.'.old'); 171 | if (! is_array($old_data)) { 172 | $old_data = array(); 173 | } 174 | $this->session->forget($old_data); 175 | $this->session->put($this->key_name.'.old', $this->session->getValue($this->key_name.'.new')); 176 | $this->session->put($this->key_name.'.new', []); 177 | } 178 | 179 | 180 | /** 181 | * Clear all data flash. 182 | * 183 | */ 184 | public function clear() 185 | { 186 | $this->session->unsetValue($this->key_name); 187 | } 188 | 189 | 190 | /** 191 | * Calling this class in a singleton way. 192 | * 193 | * @param Session|null $session 194 | * @return SessionFlash 195 | */ 196 | static function singleton(Session $session = null) 197 | { 198 | if (self::$singleton == null) { 199 | self::$singleton = new SessionFlash($session); 200 | } 201 | 202 | return self::$singleton; 203 | } 204 | } -------------------------------------------------------------------------------- /src/Session.php: -------------------------------------------------------------------------------- 1 | store = $store; 67 | 68 | // registering shutdown function, just in case someone forgets to close session 69 | register_shutdown_function(array($this, 'close')); 70 | } 71 | 72 | /** 73 | * Creates new session 74 | * 75 | * It should be called only once at the beginning. If called for existing 76 | * session it ovewrites it (clears all values etc). 77 | * It can be replaced with {@link self::open()} (called with "true" argument) 78 | * 79 | * @return bool Session opened? 80 | */ 81 | public function create() 82 | { 83 | $this->getIdHandler()->generateId(); 84 | 85 | $this->values = array(); 86 | 87 | $this->firstTrace = time(); 88 | $this->updateLastTrace(); 89 | 90 | $this->requestsCount = 1; 91 | $this->regenerationTrace = time(); 92 | 93 | $this->fingerprint = $this->generateFingerprint(); 94 | 95 | $this->opened = true; 96 | 97 | return $this->opened; 98 | } 99 | 100 | /** 101 | * Opens the session (for a given request) 102 | * 103 | * If session hasn't been created earlier with {@link self::create()} method then: 104 | * - if argument is set to true, session will be created implicitly (behaves 105 | * like PHP's native session_start()), 106 | * - otherwise, session won't be created and apprporiate listeners will be notified. 107 | * 108 | * If called earlier, then second (and next ones) call does nothing 109 | * 110 | * @param bool $createNewIfNotExists Create new session if not exists earlier? 111 | * @return bool Session opened? 112 | */ 113 | public function open($createNewIfNotExists = false) 114 | { 115 | if (!$this->isOpened()) { 116 | if ($this->getIdHandler()->issetId()) { 117 | $this->load(); 118 | 119 | if (!$this->getFirstTrace()) { 120 | $this->getEmitter()->emit(new Event\NoDataOrExpired($this)); 121 | } elseif ($this->isExpired()) { 122 | $this->getEmitter()->emit(new Event\Expired($this)); 123 | } elseif ($this->generateFingerprint() != $this->getFingerprint()) { 124 | $this->getEmitter()->emit(new Event\InvalidFingerprint($this)); 125 | } else { 126 | $this->opened = true; 127 | $this->requestsCount += 1; 128 | } 129 | } elseif ($createNewIfNotExists) { 130 | $this->create(); 131 | } 132 | } 133 | 134 | return $this->opened; 135 | } 136 | 137 | /** 138 | * Is session opened? 139 | * 140 | * @return bool 141 | */ 142 | public function isOpened() 143 | { 144 | return $this->opened; 145 | } 146 | 147 | /** 148 | * Alias of {@link self::isOpened()}. 149 | * 150 | * @return bool 151 | */ 152 | public function isOpen() 153 | { 154 | return $this->isOpened(); 155 | } 156 | 157 | /** 158 | * Is session expired? 159 | * 160 | * @return bool 161 | */ 162 | public function isExpired() 163 | { 164 | return ($this->getLastTrace() + $this->getTtl() < time()); 165 | } 166 | 167 | /** 168 | * Close the session. 169 | */ 170 | public function close() 171 | { 172 | if ($this->opened) { 173 | if ($this->shouldRegenerateId()) { 174 | $this->regenerateId(); 175 | } 176 | 177 | $this->updateLastTrace(); 178 | $this->save(); 179 | 180 | $this->values = array(); 181 | $this->opened = false; 182 | } 183 | } 184 | 185 | /** 186 | * Destroy the session. 187 | */ 188 | public function destroy() 189 | { 190 | $this->values = array(); 191 | $this->getStore()->delete($this->getId()); 192 | $this->getIdHandler()->unsetId(); 193 | } 194 | 195 | /** 196 | * Get session identifier 197 | * 198 | * @return string 199 | */ 200 | public function getId() 201 | { 202 | return $this->getIdHandler()->getId(); 203 | } 204 | 205 | /** 206 | * Regenerates session id. 207 | * 208 | * Destroys current session in store and generates new id, which will be saved 209 | * at the end of script execution (together with values). 210 | * 211 | * Id is regenerated at the most once per script execution (even if called a few times). 212 | * 213 | * Mitigates Session Fixation - use it whenever the user's privilege level changes. 214 | */ 215 | public function regenerateId() 216 | { 217 | if (!$this->idRegenerated) { 218 | $this->getStore()->delete($this->getId()); 219 | $this->getIdHandler()->generateId(); 220 | 221 | $this->regenerationTrace = time(); 222 | $this->idRegenerated = true; 223 | 224 | return true; 225 | } 226 | 227 | return false; 228 | } 229 | 230 | /** 231 | * @param Id\Handler $idHandler 232 | */ 233 | public function setIdHandler(Id\Handler $idHandler) 234 | { 235 | $this->idHandler = $idHandler; 236 | } 237 | 238 | /** 239 | * @return Id\Handler 240 | */ 241 | public function getIdHandler() 242 | { 243 | if (! $this->idHandler) { 244 | $this->idHandler = new Id\Handler(); 245 | } 246 | 247 | return $this->idHandler; 248 | } 249 | 250 | /** 251 | * @param int $limit 252 | */ 253 | public function setIdRequestsLimit($limit) 254 | { 255 | $this->idRequestsLimit = $limit; 256 | } 257 | 258 | /** 259 | * @param int $ttl 260 | */ 261 | public function setIdTtl($ttl) 262 | { 263 | $this->idTtl = $ttl; 264 | } 265 | 266 | /** 267 | * Determine if session id should be regenerated? (based on request_counter or regenerationTrace) 268 | */ 269 | protected function shouldRegenerateId() 270 | { 271 | if (($this->idRequestsLimit) && ($this->requestsCount >= $this->idRequestsLimit)) { 272 | return true; 273 | } 274 | 275 | if (($this->idTtl && $this->regenerationTrace) && ($this->regenerationTrace + $this->idTtl < time())) { 276 | return true; 277 | } 278 | 279 | return false; 280 | } 281 | 282 | /** 283 | * @return StoreInterface 284 | */ 285 | protected function getStore() 286 | { 287 | return $this->store; 288 | } 289 | 290 | /** 291 | * @param FingerprintGeneratorInterface $fingerprintGenerator 292 | */ 293 | public function addFingerprintGenerator(FingerprintGeneratorInterface $fingerprintGenerator) 294 | { 295 | $this->fingerprintGenerators[] = $fingerprintGenerator; 296 | } 297 | 298 | /** 299 | * @return string 300 | */ 301 | protected function generateFingerprint() 302 | { 303 | $fingerprint = ''; 304 | 305 | foreach ($this->fingerprintGenerators as $fingerprintGenerator) { 306 | $fingerprint .= $fingerprintGenerator->generate(); 307 | } 308 | 309 | return $fingerprint; 310 | } 311 | 312 | /** 313 | * @return string 314 | */ 315 | public function getFingerprint() 316 | { 317 | return $this->fingerprint; 318 | } 319 | 320 | /** 321 | * Gets first trace timestamp. 322 | * 323 | * @return int 324 | */ 325 | public function getFirstTrace() 326 | { 327 | return $this->firstTrace; 328 | } 329 | 330 | /** 331 | * Updates last trace timestamp. 332 | */ 333 | protected function updateLastTrace() 334 | { 335 | $this->lastTrace = time(); 336 | } 337 | 338 | /** 339 | * Gets last trace timestamp. 340 | * 341 | * @return int 342 | */ 343 | public function getLastTrace() 344 | { 345 | return $this->lastTrace; 346 | } 347 | 348 | /** 349 | * Gets last (id) regeneration timestamp. 350 | * 351 | * @return int 352 | */ 353 | public function getRegenerationTrace() 354 | { 355 | return $this->regenerationTrace; 356 | } 357 | 358 | /** 359 | * It must be called before {@link self::open()}. 360 | * 361 | * @param int $ttl 362 | * @throws Exception 363 | */ 364 | public function setTtl($ttl) 365 | { 366 | if ($this->isOpened()) { 367 | throw new Exception('Session is already opened, ttl cannot be set'); 368 | } 369 | 370 | if ($ttl < 1) { 371 | throw new Exception('$ttl must be greather than 0'); 372 | } 373 | 374 | $this->ttl = (int)$ttl; 375 | } 376 | 377 | /** 378 | * @return int 379 | */ 380 | public function getTtl() 381 | { 382 | return $this->ttl; 383 | } 384 | 385 | /** 386 | * @return int 387 | */ 388 | public function getRequestsCount() 389 | { 390 | return $this->requestsCount; 391 | } 392 | 393 | /** 394 | * Sets session value in given or default namespace 395 | * 396 | * @param string $name 397 | * @param mixed $value 398 | * @param string $namespace 399 | */ 400 | public function setValue($name, $value, $namespace = self::DEFAULT_NAMESPACE) 401 | { 402 | $this->values[$namespace][$name] = $value; 403 | } 404 | 405 | /** 406 | * Gets session value from given or default namespace 407 | * 408 | * @param string $name 409 | * @param string $namespace 410 | * @return mixed 411 | */ 412 | public function getValue($name, $namespace = self::DEFAULT_NAMESPACE) 413 | { 414 | return isset($this->values[$namespace][$name]) ? $this->values[$namespace][$name] : null; 415 | } 416 | 417 | /** 418 | * Gets and unsets value (flash value) for given or default namespace 419 | * 420 | * @param string $name 421 | * @param string $namespace 422 | * @return mixed 423 | */ 424 | public function getUnsetValue($name, $namespace = self::DEFAULT_NAMESPACE) 425 | { 426 | $value = $this->getValue($name, $namespace); 427 | $this->unsetValue($name, $namespace); 428 | 429 | return $value; 430 | } 431 | 432 | /** 433 | * Get all values for given or default namespace 434 | * 435 | * @param string $namespace 436 | * @return array 437 | */ 438 | public function getValues($namespace = self::DEFAULT_NAMESPACE) 439 | { 440 | return (isset($this->values[$namespace]) ? $this->values[$namespace] : array()); 441 | } 442 | 443 | /** 444 | * @param string $name 445 | * @param string $namespace 446 | * @return bool 447 | */ 448 | public function issetValue($name, $namespace = self::DEFAULT_NAMESPACE) 449 | { 450 | return isset($this->values[$namespace][$name]); 451 | } 452 | 453 | /** 454 | * @param string $name 455 | * @param string $namespace 456 | */ 457 | public function unsetValue($name, $namespace = self::DEFAULT_NAMESPACE) 458 | { 459 | if (isset($this->values[$namespace][$name])) { 460 | unset($this->values[$namespace][$name]); 461 | } 462 | } 463 | 464 | /** 465 | * @param string $namespace 466 | */ 467 | public function unsetValues($namespace = self::DEFAULT_NAMESPACE) 468 | { 469 | if (isset($this->values[$namespace])) { 470 | unset($this->values[$namespace]); 471 | } 472 | } 473 | 474 | /** 475 | * @param mixed $offset 476 | * @param mixed $value 477 | */ 478 | public function offsetSet($offset, $value) 479 | { 480 | $this->setValue($offset, $value); 481 | } 482 | 483 | /** 484 | * @param mixed $offset 485 | * @return mixed 486 | */ 487 | public function offsetGet($offset) 488 | { 489 | return $this->getValue($offset); 490 | } 491 | 492 | /** 493 | * @param mixed $offset 494 | * @return bool 495 | */ 496 | public function offsetExists($offset) 497 | { 498 | return $this->issetValue($offset); 499 | } 500 | 501 | /** 502 | * @param mixed $offset 503 | */ 504 | public function offsetUnset($offset) 505 | { 506 | $this->unsetValue($offset); 507 | } 508 | 509 | /** 510 | * Loads session data from defined store. 511 | * 512 | * @return bool 513 | */ 514 | protected function load() 515 | { 516 | $id = $this->getId(); 517 | $values = $this->getStore()->fetch($id); 518 | 519 | if ($values === false) { 520 | return false; 521 | } 522 | 523 | // metadata 524 | $metadata = $values[self::METADATA_NAMESPACE]; 525 | $this->firstTrace = $metadata['firstTrace']; 526 | $this->lastTrace = $metadata['lastTrace']; 527 | $this->regenerationTrace = $metadata['regenerationTrace']; 528 | $this->requestsCount = $metadata['requestsCount']; 529 | $this->fingerprint = $metadata['fingerprint']; 530 | 531 | // values 532 | $this->values = $values; 533 | 534 | return true; 535 | } 536 | 537 | /** 538 | * Saves session data into defined store. 539 | * 540 | * @return bool 541 | */ 542 | protected function save() 543 | { 544 | $this->flash()->ageFlashData(); 545 | 546 | $values = $this->values; 547 | 548 | $values[self::METADATA_NAMESPACE] = [ 549 | 'firstTrace' => $this->getFirstTrace(), 550 | 'lastTrace' => $this->getLastTrace(), 551 | 'regenerationTrace' => $this->getRegenerationTrace(), 552 | 'requestsCount' => $this->getRequestsCount(), 553 | 'fingerprint' => $this->getFingerprint(), 554 | ]; 555 | 556 | return $this->getStore()->save($this->getId(), $values, $this->ttl); 557 | } 558 | 559 | /** 560 | * Put a key / value pair or array of key / value pairs in the session. 561 | * 562 | * @param string|array $key 563 | * @param mixed $value 564 | * @return void 565 | */ 566 | public function put($key, $value = null) 567 | { 568 | if (! is_array($key)) { 569 | $key = array($key => $value); 570 | } 571 | 572 | foreach ($key as $arrayKey => $arrayValue) { 573 | $this->setValue($arrayKey, $arrayValue); 574 | } 575 | } 576 | 577 | 578 | /** 579 | * Push a value onto a session array. 580 | * 581 | * @param string $key 582 | * @param mixed $value 583 | * @return void 584 | */ 585 | public function push($key, $value) 586 | { 587 | $array = $this->getValue($key); 588 | $array[] = $value; 589 | $this->setValue($key, $array); 590 | } 591 | 592 | /** 593 | * Remove one or many items from the session. 594 | * 595 | * @param string|array $keys 596 | * @return void 597 | */ 598 | public function forget($keys) 599 | { 600 | $remove = $keys; 601 | 602 | foreach($remove as $key){ 603 | $this->unsetValue($key); 604 | } 605 | 606 | } 607 | 608 | 609 | /** 610 | * Call the flash session handler. 611 | * 612 | * @return SessionFlash 613 | */ 614 | public function flash() 615 | { 616 | if (is_null($this->flash)) { 617 | $this->flash = new SessionFlash($this); 618 | } 619 | 620 | return $this->flash; 621 | } 622 | } 623 | -------------------------------------------------------------------------------- /tests/SessionTest.php: -------------------------------------------------------------------------------- 1 | getStoreMock()); 17 | } 18 | 19 | $idHandler = $this 20 | ->getMockBuilder('\Sesshin\Id\Handler') 21 | ->setMethods( array('generateId', 'getId', 'setId', 'issetId', 'unsetId')) 22 | ->getMock(); 23 | 24 | $session->setIdHandler($idHandler); 25 | 26 | $emitter = $this 27 | ->getMockBuilder('\League\Event\Emitter') 28 | ->setMethods(array('emit', 'addListener')) 29 | ->getMock(); 30 | 31 | $session->setEmitter($emitter); 32 | 33 | return $session; 34 | } 35 | 36 | function getSessionMock(array $methods) 37 | { 38 | return $this 39 | ->getMockBuilder('\Sesshin\Session') 40 | ->setConstructorArgs([$this->getStoreMock()]) 41 | ->setMethods( $methods ) 42 | ->getMock(); 43 | } 44 | 45 | private function getStoreMock() 46 | { 47 | return $this->createMock('\Sesshin\Store\StoreInterface', array('save', 'fetch', 'delete')); 48 | } 49 | 50 | /** 51 | * @covers Sesshin\Session::setValue 52 | */ 53 | public function testValueIsSetToDefaultNamespaceByDefault() 54 | { 55 | $session = $this->setUpDefaultSession(); 56 | $ref_prop = $this->setPropertyAccessible($session, 'values'); 57 | 58 | $session->setValue('name', 'value'); 59 | 60 | $values = $ref_prop->getValue($session); 61 | $this->assertEquals('value', $values[Session::DEFAULT_NAMESPACE]['name']); 62 | } 63 | 64 | /** 65 | * @covers Sesshin\Session::setValue 66 | */ 67 | public function testCanSetValueToCustomNamespace() 68 | { 69 | $session = $this->setUpDefaultSession(); 70 | $refProp = $this->setPropertyAccessible($session, 'values'); 71 | 72 | $session->setValue('name', 'value', 'namespace'); 73 | 74 | $values = $refProp->getValue($session); 75 | $this->assertEquals('value', $values['namespace']['name']); 76 | } 77 | 78 | /** 79 | * @covers Sesshin\Session::getValue 80 | * @depends testValueIsSetToDefaultNamespaceByDefault 81 | */ 82 | public function testCanGetValue() 83 | { 84 | $session = $this->setUpDefaultSession(); 85 | $session->setValue('name', 'value'); 86 | $this->assertSame('value', $session->getValue('name')); 87 | } 88 | 89 | /** 90 | * @covers Sesshin\Session::getValue 91 | * @depends testValueIsSetToDefaultNamespaceByDefault 92 | */ 93 | public function testCanGetValueMethodReturnsNullIfNoValueForGivenName() 94 | { 95 | $session = $this->setUpDefaultSession(); 96 | $this->assertNull($session->getValue('name')); 97 | } 98 | 99 | /** 100 | * @covers Sesshin\Session::issetValue 101 | * @depends testValueIsSetToDefaultNamespaceByDefault 102 | */ 103 | public function testCanCheckIfValueIsSet() 104 | { 105 | $session = $this->setUpDefaultSession(); 106 | $session->setValue('name', 'value'); 107 | $this->assertTrue($session->issetValue('name')); 108 | } 109 | 110 | /** 111 | * @covers Sesshin\Session::unsetValue 112 | * @depends testValueIsSetToDefaultNamespaceByDefault 113 | */ 114 | public function testCanUnsetValues() 115 | { 116 | $session = $this->setUpDefaultSession(); 117 | $session->setValue('name', 'value'); 118 | $session->unsetValue('name'); 119 | $this->assertNull($session->getValue('name')); 120 | } 121 | 122 | /** 123 | * @covers Sesshin\Session::getUnsetValue 124 | */ 125 | public function testCanGetAndUnsetValue() 126 | { 127 | $session = $this->setUpDefaultSession(); 128 | $session->setValue('name', 'value'); 129 | $value = $session->getUnsetValue('name'); 130 | $this->assertEquals('value', $value); 131 | $this->assertNull($session->getValue('name')); 132 | } 133 | 134 | /** 135 | * @covers Sesshin\Session::getValues 136 | */ 137 | public function testCanGetAllValuesForNamespace() 138 | { 139 | $session = $this->setUpDefaultSession(); 140 | $session->setValue('name1', 'value1'); 141 | $session->setValue('name2', 'value2'); 142 | $this->assertEquals(array('name1' => 'value1', 'name2' => 'value2'), $session->getValues()); 143 | } 144 | 145 | /** 146 | * @covers Sesshin\Session::unsetValues 147 | */ 148 | public function testCanUnsetAllValuesForNamespace() 149 | { 150 | $session = $this->setUpDefaultSession(); 151 | $session->setValue('name1', 'value1'); 152 | $session->setValue('name2', 'value2'); 153 | $session->unsetValues(); 154 | $this->assertNull($session->getValue('name1')); 155 | $this->assertNull($session->getValue('name2')); 156 | $this->assertEmpty($session->getValues()); 157 | } 158 | 159 | /** 160 | * @covers Sesshin\Session::getRequestsCount 161 | */ 162 | public function testCanGetRequestsCount() 163 | { 164 | $session = $this->setUpDefaultSession(); 165 | $value = 37; 166 | $this->setPropertyValue($session, 'requestsCount', $value); 167 | $this->assertEquals($value, $session->getRequestsCount()); 168 | } 169 | 170 | /** 171 | * @covers Sesshin\Session::create 172 | */ 173 | public function testCreateMethodGeneratesId() 174 | { 175 | $session = $this->setUpDefaultSession(); 176 | $session->getIdHandler()->expects($this->once())->method('generateId'); 177 | $session->create(); 178 | } 179 | 180 | /** 181 | * @covers Sesshin\Session::create 182 | */ 183 | public function testCreateMethodUnsetsAllValues() 184 | { 185 | $session = $this->setUpDefaultSession(); 186 | $refPropValues = $this->setPropertyValue($session, 'values', array(1, 2, 3, 4)); 187 | $session->create(); 188 | $this->assertEmpty($refPropValues->getValue($session)); 189 | } 190 | 191 | /** 192 | * @covers Sesshin\Session::create 193 | */ 194 | public function testCreateMethodResetsFirstTrace() 195 | { 196 | $session = $this->setUpDefaultSession(); 197 | $firstTrace = $session->getFirstTrace(); 198 | $session->create(); 199 | $this->assertNotEquals($firstTrace, $session->getFirstTrace()); 200 | } 201 | 202 | /** 203 | * @covers Sesshin\Session::create 204 | */ 205 | public function testCreateMethodResetsLastTrace() 206 | { 207 | $session = $this->setUpDefaultSession(); 208 | $lastTrace = $session->getLastTrace(); 209 | $session->create(); 210 | $this->assertNotEquals($lastTrace, $session->getLastTrace()); 211 | } 212 | 213 | /** 214 | * @covers Sesshin\Session::create 215 | */ 216 | public function testCreateMethodResetsRequestsCount() 217 | { 218 | $session = $this->setUpDefaultSession(); 219 | $session->create(); 220 | $this->assertEquals(1, $session->getRequestsCount()); 221 | } 222 | 223 | /** 224 | * @covers Sesshin\Session::create 225 | */ 226 | public function testCreateMethodResetsIdRegenerationTrace() 227 | { 228 | $session = $this->setUpDefaultSession(); 229 | $regenerationTrace = $session->getRegenerationTrace(); 230 | $session->create(); 231 | $this->assertNotEquals($regenerationTrace, $session->getRegenerationTrace()); 232 | 233 | $value = 1; 234 | $session = $this->setUpDefaultSession(); 235 | $this->setPropertyValue($session, 'regenerationTrace', $value); 236 | $session->create(); 237 | $this->assertNotEquals($value, $session->getRegenerationTrace()); 238 | 239 | $this->assertGreaterThanOrEqual(time() - 1, $session->getRegenerationTrace()); 240 | } 241 | 242 | /** 243 | * @covers Sesshin\Session::create 244 | */ 245 | public function testCreateMethodGeneratesFingerprint() 246 | { 247 | $session = $this->setUpDefaultSession( 248 | $this 249 | ->getMockBuilder('\Sesshin\Session') 250 | ->setConstructorArgs([$this->getStoreMock()]) 251 | ->setMethods( array('generateFingerprint')) 252 | ->getMock() 253 | ); 254 | 255 | $session->expects($this->once())->method('generateFingerprint'); 256 | $session->create(); 257 | } 258 | 259 | /** 260 | * @covers Sesshin\Session::create 261 | */ 262 | public function testCreateMethodOpensSession() 263 | { 264 | $session = $this->setUpDefaultSession(); 265 | $session->create(); 266 | $this->assertEquals(true, $session->isOpened()); 267 | } 268 | 269 | /** 270 | * @covers Sesshin\Session::open 271 | */ 272 | public function testOpenMethodWhenCalledWithTrueThenCreatesNewSessionIfSessionNotExistsAlready() 273 | { 274 | $session = $this->setUpDefaultSession($this->getSessionMock(['create'])); 275 | $session->getIdHandler()->expects($this->any())->method('issetId')->will($this->returnValue(false)); 276 | $session->expects($this->once())->method('create'); 277 | 278 | $session->open(true); 279 | } 280 | 281 | /** 282 | * @covers Sesshin\Session::open 283 | */ 284 | public function testOpenMethodWhenCalledWithTrueThenDoesNotCreateNewSessionIfSessionIdExistsAlready() 285 | { 286 | $session = $this->setUpDefaultSession($this->getSessionMock(['issetId','create'])); 287 | $session->getIdHandler()->expects($this->any())->method('issetId')->will($this->returnValue(true)); 288 | $session->expects($this->never())->method('create'); 289 | 290 | $session->open(true); 291 | } 292 | 293 | /** 294 | * @covers Sesshin\Session::open 295 | */ 296 | public function testOpenMethodWhenCalledWithFalseThenDoesNotCreateNewSession() 297 | { 298 | $session = $this->setUpDefaultSession($this->createMock('\Sesshin\Session', array('create'), [$this->getStoreMock()])); 299 | $session->expects($this->never())->method('create'); 300 | 301 | $session->open(false); 302 | } 303 | 304 | /** 305 | * @covers Sesshin\Session::open 306 | */ 307 | public function testOpenMethodLoadsSessionDataIfSessionExists() 308 | { 309 | $session = $this->setUpDefaultSession($this->getSessionMock(array('create', 'load','isOpened'))); 310 | $session->expects($this->any())->method('isOpened')->will($this->returnValue(false)); 311 | $session->getIdHandler()->expects($this->any())->method('issetId')->will($this->returnValue(true)); 312 | $session->expects($this->once())->method('load'); 313 | 314 | $session->open(); 315 | } 316 | 317 | /** 318 | * @covers Sesshin\Session::open 319 | */ 320 | public function testOpenMethodDoesNotLoadSessionDataIfSessionNotExists() 321 | { 322 | $session = $this->setUpDefaultSession($this->getSessionMock(array('isOpened', 'issetId','load'))); 323 | $session->expects($this->any())->method('isOpened')->will($this->returnValue(false)); 324 | $session->getIdHandler()->expects($this->any())->method('issetId')->will($this->returnValue(false)); 325 | $session->expects($this->never())->method('load'); 326 | 327 | $session->open(); 328 | } 329 | 330 | /** 331 | * @covers Sesshin\Session::open 332 | */ 333 | public function testOpenMethodTriggersSessionNoDataOrExpiredEventIfNoDataPresentAfterLoad() 334 | { 335 | $session = $this->setUpDefaultSession($this->getSessionMock( array('isOpened', 'issetId', 'getFirstTrace','emit') )); 336 | $session->expects($this->any())->method('isOpened')->will($this->returnValue(false)); 337 | $session->getIdHandler()->expects($this->any())->method('issetId')->will($this->returnValue(true)); 338 | $session->expects($this->once())->method('getFirstTrace')->will($this->returnValue(false)); 339 | $session->getEmitter()->expects($this->once())->method('emit')->with($this->equalTo(new Event\NoDataOrExpired($session))); 340 | 341 | $session->open(); 342 | } 343 | 344 | /** 345 | * @covers Sesshin\Session::open 346 | */ 347 | public function testOpenMethodTriggersSessionExpiredEventIfSessionExpired() 348 | { 349 | $session = $this->setUpDefaultSession($this->getSessionMock(array('isOpened', 'issetId', 'getFirstTrace', 'isExpired','emit'))); 350 | $session->expects($this->any())->method('isOpened')->will($this->returnValue(false)); 351 | $session->getIdHandler()->expects($this->any())->method('issetId')->will($this->returnValue(true)); 352 | $session->expects($this->once())->method('getFirstTrace')->will($this->returnValue(time())); 353 | $session->expects($this->once())->method('isExpired')->will($this->returnValue(true)); 354 | $session->getEmitter()->expects($this->once())->method('emit')->with($this->equalTo(new Event\Expired($session))); 355 | 356 | $session->open(); 357 | } 358 | 359 | /** 360 | * Fingerpring is generated, so it can be compared with the one in session 361 | * metadata for session validity. 362 | * 363 | * @covers Sesshin\Session::open 364 | */ 365 | public function testOpenMethodTriggersInvalidFingerprintEventIfLoadedFingerprintInvalid() 366 | { 367 | $session = $this->setUpDefaultSession($this->getSessionMock( array('getSessionMock','issetId', 'load', 'getFirstTrace', 'isExpired', 'getFingerprint', 'generateFingerprint','isOpened'))); 368 | $session->expects($this->any())->method('isOpened')->will($this->returnValue(false)); 369 | $session->getIdHandler()->expects($this->any())->method('issetId')->will($this->returnValue(true)); 370 | $session->expects($this->once())->method('getFirstTrace')->will($this->returnValue(time())); 371 | $session->expects($this->once())->method('isExpired')->will($this->returnValue(false)); 372 | $session->expects($this->once())->method('getFingerprint')->will($this->returnValue('abc')); 373 | $session->expects($this->once())->method('generateFingerprint')->will($this->returnValue('def')); 374 | $session->getEmitter()->expects($this->once())->method('emit')->with($this->equalTo(new Event\InvalidFingerprint($session))); 375 | 376 | $session->open(); 377 | } 378 | 379 | /** 380 | * @covers Sesshin\Session::open 381 | */ 382 | public function testOpenMethodOpenSessionAndIncrementsRequestsCounter() 383 | { 384 | $session = $this->setUpDefaultSession($this->getSessionMock(array('create','isOpened', 'issetId', 'getFirstTrace', 'isExpired', 'getFingerprint', 'generateFingerprint'))); 385 | $session->expects($this->any())->method('isOpened')->will($this->returnValue(false)); 386 | $session->getIdHandler()->expects($this->any())->method('issetId')->will($this->returnValue(true)); 387 | $session->expects($this->once())->method('getFirstTrace')->will($this->returnValue(time())); 388 | $session->expects($this->once())->method('isExpired')->will($this->returnValue(false)); 389 | $session->expects($this->once())->method('getFingerprint')->will($this->returnValue('abc')); 390 | $session->expects($this->once())->method('generateFingerprint')->will($this->returnValue('abc')); 391 | 392 | $requests_counter = $session->getRequestsCount(); 393 | 394 | $session->open(); 395 | 396 | $this->assertSame(false, $session->isOpened()); 397 | $this->assertEquals($requests_counter + 1, $session->getRequestsCount()); 398 | } 399 | 400 | /** 401 | * @covers Sesshin\Session::isOpen 402 | * @covers Sesshin\Session::isOpened 403 | */ 404 | public function testCanCheckIfSessionIsOpened() 405 | { 406 | $session = $this->setUpDefaultSession(); 407 | $this->setPropertyValue($session, 'opened', true); 408 | $this->assertSame(true, $session->isOpen()); 409 | $this->assertSame(true, $session->isOpened()); 410 | $this->setPropertyValue($session, 'opened', false); 411 | $this->assertSame(false, $session->isOpen()); 412 | $this->assertSame(false, $session->isOpened()); 413 | } 414 | 415 | /** 416 | * @covers Sesshin\Session::setIdHandler 417 | * @covers Sesshin\Session::getIdHandler 418 | */ 419 | public function testCanSetGetIdHandler() 420 | { 421 | $session = new Session($this->getStoreMock()); 422 | $id_handler = new \Sesshin\Id\Handler(); 423 | $session->setIdHandler($id_handler); 424 | $this->assertSame($id_handler, $session->getIdHandler()); 425 | } 426 | 427 | /** 428 | * @covers Sesshin\Session::getIdHandler 429 | */ 430 | public function testUsesDefaultIdHandlerIfNotSet() 431 | { 432 | $session = new Session($this->getStoreMock()); 433 | $this->assertEquals('Sesshin\Id\Handler', get_class($session->getIdHandler())); 434 | } 435 | 436 | /** 437 | * @covers Sesshin\Session::shouldRegenerateId 438 | */ 439 | public function testSessionIdShouldBeRegeneratedIfIdRequestsLimitReached() 440 | { 441 | $session = new Session($this->getStoreMock()); 442 | $this->setPropertyValue($session, 'requestsCount', 5); 443 | $this->setPropertyValue($session, 'idRequestsLimit', 5); 444 | $this->assertSame(true, $this->invokeMethod($session, 'shouldRegenerateId')); 445 | } 446 | 447 | /** 448 | * @covers Sesshin\Session::shouldRegenerateId 449 | */ 450 | public function testSessionIdShouldBeRegeneratedIfIdTtlLimitReached() 451 | { 452 | $session = new Session($this->getStoreMock()); 453 | $this->setPropertyValue($session, 'idTtl', 60); 454 | $this->setPropertyValue($session, 'regenerationTrace', time() - 90); 455 | $this->assertSame(true, $this->invokeMethod($session, 'shouldRegenerateId')); 456 | } 457 | 458 | /** 459 | * @covers Sesshin\Session::shouldRegenerateId 460 | */ 461 | public function testSessionIdShouldNotBeRegeneratedIfLimitsNotReached() 462 | { 463 | $session = new Session($this->getStoreMock()); 464 | $this->setPropertyValue($session, 'requestsCount', 5); 465 | $this->setPropertyValue($session, 'idRequestsLimit', 6); 466 | $this->setPropertyValue($session, 'idTtl', 60); 467 | $this->setPropertyValue($session, 'regenerationTrace', time() - 30); 468 | $this->assertSame(false, $this->invokeMethod($session, 'shouldRegenerateId')); 469 | } 470 | 471 | /** 472 | * @covers Sesshin\Session::shouldRegenerateId 473 | */ 474 | public function testSessionIdShouldNotBeRegeneratedIfLimitsNotSet() 475 | { 476 | $session = new Session($this->getStoreMock()); 477 | $this->assertSame(false, $this->invokeMethod($session, 'shouldRegenerateId')); 478 | } 479 | 480 | /** 481 | * @covers Sesshin\Session::setEmitter 482 | * @covers Sesshin\Session::getEmitter 483 | */ 484 | public function testCanSetGetEmitter() 485 | { 486 | $session = new Session($this->getStoreMock()); 487 | $emitter = new \League\Event\Emitter(); 488 | $session->setEmitter($emitter); 489 | $this->assertSame($emitter, $session->getEmitter()); 490 | } 491 | 492 | /** 493 | * @covers Sesshin\Session::getEmitter 494 | */ 495 | public function testUsesDefaultEmitterIfNotSet() 496 | { 497 | $session = new Session($this->getStoreMock()); 498 | $this->assertEquals('League\Event\Emitter', get_class($session->getEmitter())); 499 | } 500 | 501 | /** 502 | * @covers Sesshin\Session::offsetSet 503 | * @covers Sesshin\Session::offsetGet 504 | * @covers Sesshin\Session::offsetExists 505 | * @covers Sesshin\Session::offsetUnset 506 | */ 507 | public function testImplementsArrayAccessForSessionValues() 508 | { 509 | $session = $this->getSessionMock(array('setValue')); 510 | $session->expects($this->once())->method('setValue')->with($this->equalTo('key'), $this->equalTo('value')); 511 | $session['key'] = 'value'; 512 | 513 | $session = $this->getSessionMock(array('getValue')); 514 | $session->expects($this->once())->method('getValue')->with($this->equalTo('key')); 515 | $session['key']; 516 | 517 | $session = $this->getSessionMock(array('issetValue')); 518 | $session->expects($this->once())->method('issetValue')->with($this->equalTo('key')); 519 | isset($session['key']); 520 | 521 | $session = $this->getSessionMock(array('unsetValue')); 522 | $session->expects($this->once())->method('unsetValue')->with($this->equalTo('key')); 523 | unset($session['key']); 524 | } 525 | 526 | /** 527 | * @covers Sesshin\Session::load 528 | * @depends testOpenMethodLoadsSessionDataIfSessionExists 529 | */ 530 | public function testLoadMethodFetchesDataFromStore() 531 | { 532 | $store = $this->getStoreMock(); 533 | $session = $this->setUpDefaultSession(new Session($store)); 534 | 535 | $store->expects($this->any())->method('fetch')->with($this->equalTo($session->getId())); 536 | $load_result = $this->invokeMethod($session, 'load'); 537 | 538 | $this->assertTrue(true,$load_result); 539 | } 540 | 541 | /** 542 | * @covers Sesshin\Session::load 543 | */ 544 | public function testLoadMethodReturnsFalseIfNoDataInStore() 545 | { 546 | $store = $this->getStoreMock(); 547 | $session = $this->setUpDefaultSession(new Session($store)); 548 | 549 | $store->expects($this->any())->method('fetch')->will($this->returnValue(false)); 550 | $this->assertFalse($this->invokeMethod($session, 'load')); 551 | } 552 | 553 | 554 | function testPutMethodDataForSession() 555 | { 556 | $store = $this->getStoreMock(); 557 | $session = $this->setUpDefaultSession(new Session($store)); 558 | $session->put([ 559 | 'key1'=>'value1', 560 | 'key2'=>'value2' 561 | ]); 562 | 563 | $this->assertEquals('value1', $session->getValue('key1')); 564 | $this->assertEquals('value2', $session->getValue('key2')); 565 | } 566 | 567 | 568 | function testPushMethodForArrayData() 569 | { 570 | $store = $this->getStoreMock(); 571 | $session = $this->setUpDefaultSession(new Session($store)); 572 | 573 | $session->push('users','value1'); 574 | $session->push('users','value2'); 575 | $data_users = $session->getValue('users'); 576 | 577 | $this->assertEquals('value1', $data_users[0]); 578 | $this->assertEquals('value2', $data_users[1]); 579 | } 580 | 581 | 582 | function testForgetDataFromSession() 583 | { 584 | $store = $this->getStoreMock(); 585 | $session = $this->setUpDefaultSession(new Session($store)); 586 | $session->put([ 587 | 'key1'=>'value1', 588 | 'key2'=>'value2' 589 | ]); 590 | 591 | $this->assertEquals('value1', $session->getValue('key1')); 592 | $this->assertEquals('value2', $session->getValue('key2')); 593 | 594 | $session->forget(['key1']); 595 | 596 | $this->assertEmpty($session->getValue('key1')); 597 | 598 | } 599 | 600 | } 601 | --------------------------------------------------------------------------------