├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── AbstractContainer.php ├── AbstractManager.php ├── Config ├── ConfigInterface.php ├── SessionConfig.php └── StandardConfig.php ├── ConfigProvider.php ├── Container.php ├── Exception ├── BadMethodCallException.php ├── ExceptionInterface.php ├── InvalidArgumentException.php └── RuntimeException.php ├── ManagerInterface.php ├── Module.php ├── SaveHandler ├── Cache.php ├── DbTableGateway.php ├── DbTableGatewayOptions.php ├── MongoDB.php ├── MongoDBOptions.php └── SaveHandlerInterface.php ├── Service ├── ContainerAbstractServiceFactory.php ├── SessionConfigFactory.php ├── SessionManagerFactory.php └── StorageFactory.php ├── SessionManager.php ├── Storage ├── AbstractSessionArrayStorage.php ├── ArrayStorage.php ├── Factory.php ├── SessionArrayStorage.php ├── SessionStorage.php ├── StorageInitializationInterface.php └── StorageInterface.php ├── Validator ├── AbstractValidatorChainEM2.php ├── AbstractValidatorChainEM3.php ├── HttpUserAgent.php ├── Id.php ├── RemoteAddr.php ├── ValidatorChainTrait.php └── ValidatorInterface.php ├── ValidatorChain.php └── compatibility └── autoload.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 2.9.2 - TBD 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - Nothing. 14 | 15 | ### Deprecated 16 | 17 | - Nothing. 18 | 19 | ### Removed 20 | 21 | - Nothing. 22 | 23 | ### Fixed 24 | 25 | - Nothing. 26 | 27 | ## 2.9.1 - 2019-10-28 28 | 29 | ### Added 30 | 31 | - Nothing. 32 | 33 | ### Changed 34 | 35 | - Nothing. 36 | 37 | ### Deprecated 38 | 39 | - Nothing. 40 | 41 | ### Removed 42 | 43 | - Nothing. 44 | 45 | ### Fixed 46 | 47 | - [#123](https://github.com/zendframework/zend-session/pull/123) fixes a bug preventing two first hash functions from `hash_algos()` 48 | (usually `md2` and `md4`) from being used in `SessionConfig::setHashFunction`. 49 | 50 | ## 2.9.0 - 2019-09-20 51 | 52 | ### Added 53 | 54 | - [#115](https://github.com/zendframework/zend-session/pull/115) adds support for PHP 7.3. 55 | 56 | ### Changed 57 | 58 | - Nothing. 59 | 60 | ### Deprecated 61 | 62 | - Nothing. 63 | 64 | ### Removed 65 | 66 | - [#115](https://github.com/zendframework/zend-session/pull/115) removes support for zend-stdlib v2 releases. 67 | 68 | ### Fixed 69 | 70 | - Nothing. 71 | 72 | ## 2.8.7 - 2019-09-19 73 | 74 | ### Added 75 | 76 | - Nothing. 77 | 78 | ### Changed 79 | 80 | - Nothing. 81 | 82 | ### Deprecated 83 | 84 | - Nothing. 85 | 86 | ### Removed 87 | 88 | - Nothing. 89 | 90 | ### Fixed 91 | 92 | - [#122](https://github.com/zendframework/zend-session/pull/122) fixes 93 | type check for configuration of session storage. Allows input to be 94 | an instance of ArrayAccess or an array. 95 | 96 | ## 2.8.6 - 2019-08-11 97 | 98 | ### Added 99 | 100 | - Nothing. 101 | 102 | ### Changed 103 | 104 | - Nothing. 105 | 106 | ### Deprecated 107 | 108 | - Nothing. 109 | 110 | ### Removed 111 | 112 | - Nothing. 113 | 114 | ### Fixed 115 | 116 | - [#120](https://github.com/zendframework/zend-session/pull/120) fixes issue 117 | "Commands out of sync; you can't run this command now" with DbTableGateway 118 | save handler while using Mysqli adapter. 119 | 120 | - [#106](https://github.com/zendframework/zend-session/pull/106) fixes issue 121 | with Garbage collection of MongoDB save handler where maxlifetime 122 | is provided in seconds. 123 | 124 | - [#114](https://github.com/zendframework/zend-session/pull/114) fixes 125 | Validator\Id compatibility with PHP 7.1. INI setting `session.sid_bits_per_character` 126 | can be now used with PHP 7.1+ instead of `session.hash_bits_per_character` 127 | (used with PHP versions prior to 7.1). 128 | 129 | In some very specific situations this can lead to an issue with previously generated sessions. 130 | See issue [#121](https://github.com/zendframework/zend-session/issues/121). 131 | 132 | - [#118](https://github.com/zendframework/zend-session/pull/118) avoid unnecessary phpinfo() call 133 | when register own save handler which is an object. 134 | 135 | ## 2.8.5 - 2018-02-22 136 | 137 | ### Added 138 | 139 | - Nothing. 140 | 141 | ### Changed 142 | 143 | - Nothing. 144 | 145 | ### Deprecated 146 | 147 | - Nothing. 148 | 149 | ### Removed 150 | 151 | - Nothing. 152 | 153 | ### Fixed 154 | 155 | - [#108](https://github.com/zendframework/zend-session/pull/108) fixes a dependency 156 | conflict in `composer.json` which prevented `phpunit/phpunit` 6.5 or newer from 157 | being installed together with `zendframework/zend-session`. 158 | 159 | ## 2.8.4 - 2018-01-31 160 | 161 | ### Added 162 | 163 | - Nothing. 164 | 165 | ### Changed 166 | 167 | - Nothing. 168 | 169 | ### Deprecated 170 | 171 | - Nothing. 172 | 173 | ### Removed 174 | 175 | - Nothing. 176 | 177 | ### Fixed 178 | 179 | - [#107](https://github.com/zendframework/zend-session/pull/107) fixes an error 180 | raised by `ini_set()` within `SessionConfig::setStorageOption()` that occurs 181 | for certain INI values that cannot be set if the session is active. When this 182 | situation occurs, the class performs a `session_write_close()`, sets the new 183 | INI value, and then restarts the session. As such, we recommend that you 184 | either set production INI values in your production `php.ini`, and/or always 185 | pass your fully configured session manager to container instances you create. 186 | 187 | - [#105](https://github.com/zendframework/zend-session/pull/105) fixes an edge 188 | case whereby if the special `__ZF` session value is a non-array value, 189 | initializing the session would result in errors. 190 | 191 | - [#102](https://github.com/zendframework/zend-session/pull/102) fixes an issue 192 | introduced with 2.8.0 with `AbstractContainer::offsetGet`. Starting in 2.8.0, 193 | if the provided `$key` did not exist, the method would raise an error 194 | regarding an invalid variable reference; this release provides a fix that 195 | resolves that issue. 196 | 197 | ## 2.8.3 - 2017-12-01 198 | 199 | ### Added 200 | 201 | - Nothing. 202 | 203 | ### Changed 204 | 205 | - Nothing. 206 | 207 | ### Deprecated 208 | 209 | - Nothing. 210 | 211 | ### Removed 212 | 213 | - Nothing. 214 | 215 | ### Fixed 216 | 217 | - [#101](https://github.com/zendframework/zend-session/pull/101) fixes an issue 218 | created with the 2.8.2 release with regards to setting a session save path for 219 | non-files save handlers; prior to this patch, incorrect validations were run 220 | on the path provided, leading to unexpected exceptions being raised. 221 | 222 | ## 2.8.2 - 2017-11-29 223 | 224 | ### Added 225 | 226 | - Nothing. 227 | 228 | ### Changed 229 | 230 | - Nothing. 231 | 232 | ### Deprecated 233 | 234 | - Nothing. 235 | 236 | ### Removed 237 | 238 | - Nothing. 239 | 240 | ### Fixed 241 | 242 | - [#85](https://github.com/zendframework/zend-session/pull/85) fixes an issue 243 | with how the expiration seconds are handled when a long-running request 244 | occurs. Previously, when called, it would use the value of 245 | `$_SERVER['REQUEST_TIME']` to calculate the expiration time; this would cause 246 | failures if the expiration seconds had been reached by the time the value was 247 | set. It now correctly uses the current `time()`. 248 | 249 | - [#99](https://github.com/zendframework/zend-session/pull/99) fixes how 250 | `Zend\Session\Config\SessionConfig` handles attaching save handlers to ensure 251 | it will honor any handlers registered with the PHP engine (e.g., redis, 252 | rediscluster, etc.). 253 | 254 | ## 2.8.1 - 2017-11-28 255 | 256 | ### Added 257 | 258 | - [#92](https://github.com/zendframework/zend-session/pull/92) adds PHP 7.2 259 | support. 260 | 261 | ### Deprecated 262 | 263 | - Nothing. 264 | 265 | ### Removed 266 | 267 | - Nothing. 268 | 269 | ### Fixed 270 | 271 | - [#57](https://github.com/zendframework/zend-session/pull/57) and 272 | [#93](https://github.com/zendframework/zend-session/pull/93) provide a fix 273 | for when data found in the session is a `Traversable`; such data is now cast 274 | to an array before merging with new data. 275 | 276 | ## 2.8.0 - 2017-06-19 277 | 278 | ### Added 279 | 280 | - [#78](https://github.com/zendframework/zend-session/pull/78) adds support for 281 | PHP 7.1, and specifically the following options: 282 | - `session.sid_length` 283 | - `session.sid_bits_per_character` 284 | 285 | ### Changed 286 | 287 | - [#73](https://github.com/zendframework/zend-session/pull/73) modifies the 288 | `SessionManagerFactory` to take into account the `$requestedName`; if the 289 | `$requestedName` is the name of a class that implements `ManagerInterface`, 290 | that class will be instantiated instead of `SessionManager`, but using the 291 | same arguments (`$config, $storage, $savehandler, $validators, $options`). 292 | 293 | - [#78](https://github.com/zendframework/zend-session/pull/78) updates the 294 | `SessionConfig` class to emit deprecation notices under PHP 7.1+ when a user 295 | attempts to set INI options no longer supported by PHP 7.1+, including: 296 | - `session.entropy_file` 297 | - `session.entropy_length` 298 | - `session.hash_function` 299 | - `session.hash_bits_per_character` 300 | 301 | ### Deprecated 302 | 303 | - Nothing. 304 | 305 | ### Removed 306 | 307 | - [#78](https://github.com/zendframework/zend-session/pull/78) removes support 308 | for PHP 5.5. 309 | 310 | - [#78](https://github.com/zendframework/zend-session/pull/78) removes support 311 | for HHVM. 312 | 313 | ### Fixed 314 | 315 | - Nothing. 316 | 317 | ## 2.7.4 - 2017-06-19 318 | 319 | ### Added 320 | 321 | - Nothing. 322 | 323 | ### Deprecated 324 | 325 | - Nothing. 326 | 327 | ### Removed 328 | 329 | - Nothing. 330 | 331 | ### Fixed 332 | 333 | - [#66](https://github.com/zendframework/zend-session/pull/66) fixes how the 334 | `Cache` save handler's `destroy()` method works, ensuring it does not attempt 335 | to remove an item by `$id` if it does not already exist in the cache. 336 | - [#79](https://github.com/zendframework/zend-session/pull/79) updates the 337 | signature of `AbstractContainer::offsetGet()` to match 338 | `Zend\Stdlib\ArrayObject` and return by reference, fixing an issue when 339 | running under PHP 7.1+. 340 | 341 | ## 2.7.3 - 2016-07-05 342 | 343 | ### Added 344 | 345 | - Nothing. 346 | 347 | ### Deprecated 348 | 349 | - Nothing. 350 | 351 | ### Removed 352 | 353 | - Nothing. 354 | 355 | ### Fixed 356 | 357 | - [#51](https://github.com/zendframework/zend-session/pull/51) provides a fix to 358 | the `DbTableGateway` save handler to prevent infinite recursion when 359 | attempting to destroy an expired record during initial read operations. 360 | - [#45](https://github.com/zendframework/zend-session/pull/45) updates the 361 | `SessionManager::regenerateId()` method to only regenerate the identifier if a 362 | session already exists. 363 | 364 | ## 2.7.2 - 2016-06-24 365 | 366 | ### Added 367 | 368 | - Nothing. 369 | 370 | ### Deprecated 371 | 372 | - Nothing. 373 | 374 | ### Removed 375 | 376 | - Nothing. 377 | 378 | ### Fixed 379 | 380 | - [#46](https://github.com/zendframework/zend-session/pull/46) provides fixes to 381 | each of the `Cache` and `DbTaleGateway` save handlers to ensure they work 382 | when used under PHP 7. 383 | 384 | ## 2.7.1 - 2016-05-11 385 | 386 | ### Added 387 | 388 | - [#40](https://github.com/zendframework/zend-session/pull/40) adds and 389 | publishes the documentation to https://zendframework.github.io/zend-session/ 390 | 391 | ### Deprecated 392 | 393 | - Nothing. 394 | 395 | ### Removed 396 | 397 | - Nothing. 398 | 399 | ### Fixed 400 | 401 | - [#38](https://github.com/zendframework/zend-session/pull/38) ensures that the 402 | value from `session.gc_maxlifetime` is cast to an integer before assigning 403 | it as the `lifetime` value in the `MongoDB` adapter, ensuring sessions may be 404 | deleted. 405 | 406 | ## 2.7.0 - 2016-04-12 407 | 408 | ### Added 409 | 410 | - [#23](https://github.com/zendframework/zend-session/pull/23) provides a new 411 | `Id` validator to ensure that the session identifier is not malformed. This 412 | validator is now enabled by default; to disable it, pass 413 | `['attach_default_validators' => false]` as the fifth argument to 414 | `SessionManager`, or pass an `options` array with that value under the 415 | `session_manager` configuration key. 416 | - [#34](https://github.com/zendframework/zend-session/pull/34) adds the option 417 | to use `exporeAfterSeconds` with the `MongoDB` save handler. 418 | - [#37](https://github.com/zendframework/zend-session/pull/37) exposes the 419 | package as a standalone config-provider/component, adding: 420 | - `Zend\Session\ConfigProvider`, which maps the default services offered by 421 | the package, including the `ContainerAbstractServiceFactory`. 422 | - `Zend\Session\Module`, which does the same, but for zend-mvc contexts. 423 | 424 | ### Deprecated 425 | 426 | - Nothing. 427 | 428 | ### Removed 429 | 430 | - Nothing. 431 | 432 | ### Fixed 433 | 434 | - [#34](https://github.com/zendframework/zend-session/pull/34) updates the 435 | component to use ext/mongodb + the MongoDB PHP client library, instead of 436 | ext/mongo, for purposes of the `MongoDB` save handler, allowing the component 437 | to be used with modern MongoDB installations. 438 | 439 | ## 2.6.2 - 2016-02-25 440 | 441 | ### Added 442 | 443 | - Nothing. 444 | 445 | ### Deprecated 446 | 447 | - Nothing. 448 | 449 | ### Removed 450 | 451 | - Nothing. 452 | 453 | ### Fixed 454 | 455 | - [#32](https://github.com/zendframework/zend-session/pull/32) provides a better 456 | polfill for the `ValidatorChain` to ensure it can be represented in 457 | auto-generated classmaps (e.g., via `composer dump-autoload --optimize` and/or 458 | `composer dump-autoload --classmap-authoritative`). 459 | 460 | ## 2.6.1 - 2016-02-23 461 | 462 | ### Added 463 | 464 | - Nothing. 465 | 466 | ### Deprecated 467 | 468 | - Nothing. 469 | 470 | ### Removed 471 | 472 | - Nothing. 473 | 474 | ### Fixed 475 | 476 | - [#29](https://github.com/zendframework/zend-session/pull/29) extracts the 477 | constructor defined in `Zend\Session\Validator\ValidatorChainTrait` and pushes 478 | it into each of the `ValidatorChainEM2` and `ValidatorChainEM3` 479 | implementations, to prevent colliding constructor definitions due to 480 | inheritance + trait usage. 481 | 482 | ## 2.6.0 - 2016-02-23 483 | 484 | ### Added 485 | 486 | - [#29](https://github.com/zendframework/zend-session/pull/29) adds two new 487 | classes: `Zend\Session\Validator\ValidatorChainEM2` and `ValidatorChainEM3`. 488 | Due to differences in the `EventManagerInterface::attach()` method between 489 | zend-eventmanager v2 and v3, and the fact that `ValidatorChain` overrides that 490 | method, we now need an implementation targeting each major version. To provide 491 | a consistent use case, we use a polyfill that aliases the appropriate version 492 | to the `Zend\Session\ValidatorChain` class. 493 | 494 | ### Deprecated 495 | 496 | - Nothing. 497 | 498 | ### Removed 499 | 500 | - Nothing. 501 | 502 | ### Fixed 503 | 504 | - [#29](https://github.com/zendframework/zend-session/pull/29) updates the code 505 | to be forwards compatible with the v3 releases of zend-eventmanager and 506 | zend-servicemanager. 507 | - [#7](https://github.com/zendframework/zend-session/pull/7) Mongo save handler 508 | was using sprintf formatting without sprintf. 509 | 510 | ## 2.5.2 - 2015-07-29 511 | 512 | ### Added 513 | 514 | - Nothing. 515 | 516 | ### Deprecated 517 | 518 | - Nothing. 519 | 520 | ### Removed 521 | 522 | - Nothing. 523 | 524 | ### Fixed 525 | 526 | - [#3](https://github.com/zendframework/zend-session/pull/3) Utilize 527 | SaveHandlerInterface vs. our own. 528 | 529 | - [#2](https://github.com/zendframework/zend-session/pull/2) detect session 530 | exists by use of *PHP_SESSION_ACTIVE* 531 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2019, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zend-session 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [laminas/laminas-session](https://github.com/laminas/laminas-session). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-session.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-session) 8 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-session/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-session?branch=master) 9 | 10 | zend-session manages PHP sessions using an object oriented interface. 11 | 12 | - File issues at https://github.com/zendframework/zend-session/issues 13 | - Documentation is at https://docs.zendframework.com/zend-session/ 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zendframework/zend-session", 3 | "description": "Object-oriented interface to PHP sessions and storage", 4 | "license": "BSD-3-Clause", 5 | "keywords": [ 6 | "zf", 7 | "zendframework", 8 | "session" 9 | ], 10 | "support": { 11 | "docs": "https://docs.zendframework.com/zend-session/", 12 | "issues": "https://github.com/zendframework/zend-session/issues", 13 | "source": "https://github.com/zendframework/zend-session", 14 | "rss": "https://github.com/zendframework/zend-session/releases.atom", 15 | "chat": "https://zendframework-slack.herokuapp.com", 16 | "forum": "https://discourse.zendframework.com/c/questions/components" 17 | }, 18 | "require": { 19 | "php": "^5.6 || ^7.0", 20 | "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", 21 | "zendframework/zend-stdlib": "^3.2.1" 22 | }, 23 | "require-dev": { 24 | "container-interop/container-interop": "^1.1", 25 | "mongodb/mongodb": "^1.0.1", 26 | "php-mock/php-mock-phpunit": "^1.1.2 || ^2.0", 27 | "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16", 28 | "zendframework/zend-cache": "^2.6.1", 29 | "zendframework/zend-coding-standard": "~1.0.0", 30 | "zendframework/zend-db": "^2.7", 31 | "zendframework/zend-http": "^2.5.4", 32 | "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", 33 | "zendframework/zend-validator": "^2.6" 34 | }, 35 | "suggest": { 36 | "mongodb/mongodb": "If you want to use the MongoDB session save handler", 37 | "zendframework/zend-cache": "Zend\\Cache component", 38 | "zendframework/zend-db": "Zend\\Db component", 39 | "zendframework/zend-http": "Zend\\Http component", 40 | "zendframework/zend-servicemanager": "Zend\\ServiceManager component", 41 | "zendframework/zend-validator": "Zend\\Validator component" 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "Zend\\Session\\": "src/" 46 | } 47 | }, 48 | "autoload-dev": { 49 | "files": [ 50 | "test/autoload.php" 51 | ], 52 | "psr-4": { 53 | "ZendTest\\Session\\": "test/" 54 | } 55 | }, 56 | "config": { 57 | "sort-packages": true 58 | }, 59 | "extra": { 60 | "branch-alias": { 61 | "dev-master": "2.9.x-dev", 62 | "dev-develop": "2.10.x-dev" 63 | }, 64 | "zf": { 65 | "component": "Zend\\Session", 66 | "config-provider": "Zend\\Session\\ConfigProvider" 67 | } 68 | }, 69 | "scripts": { 70 | "check": [ 71 | "@cs-check", 72 | "@test" 73 | ], 74 | "cs-check": "phpcs", 75 | "cs-fix": "phpcbf", 76 | "test": "phpunit --colors=always", 77 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/AbstractContainer.php: -------------------------------------------------------------------------------- 1 | name = $name; 75 | $this->setManager($manager); 76 | 77 | // Create namespace 78 | parent::__construct([], ArrayObject::ARRAY_AS_PROPS); 79 | 80 | // Start session 81 | $this->getManager()->start(); 82 | } 83 | 84 | /** 85 | * Set the default ManagerInterface instance to use when none provided to constructor 86 | * 87 | * @param Manager $manager 88 | * @return void 89 | */ 90 | public static function setDefaultManager(Manager $manager = null) 91 | { 92 | static::$defaultManager = $manager; 93 | } 94 | 95 | /** 96 | * Get the default ManagerInterface instance 97 | * 98 | * If none provided, instantiates one of type {@link $managerDefaultClass} 99 | * 100 | * @return Manager 101 | * @throws Exception\InvalidArgumentException if invalid manager default class provided 102 | */ 103 | public static function getDefaultManager() 104 | { 105 | if (null === static::$defaultManager) { 106 | $manager = new static::$managerDefaultClass(); 107 | if (! $manager instanceof Manager) { 108 | throw new Exception\InvalidArgumentException( 109 | 'Invalid default manager type provided; must implement ManagerInterface' 110 | ); 111 | } 112 | static::$defaultManager = $manager; 113 | } 114 | 115 | return static::$defaultManager; 116 | } 117 | 118 | /** 119 | * Get container name 120 | * 121 | * @return string 122 | */ 123 | public function getName() 124 | { 125 | return $this->name; 126 | } 127 | 128 | /** 129 | * Set session manager 130 | * 131 | * @param null|Manager $manager 132 | * @return Container 133 | * @throws Exception\InvalidArgumentException 134 | */ 135 | protected function setManager(Manager $manager = null) 136 | { 137 | if (null === $manager) { 138 | $manager = static::getDefaultManager(); 139 | if (! $manager instanceof Manager) { 140 | throw new Exception\InvalidArgumentException( 141 | 'Manager provided is invalid; must implement ManagerInterface' 142 | ); 143 | } 144 | } 145 | $this->manager = $manager; 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * Get manager instance 152 | * 153 | * @return Manager 154 | */ 155 | public function getManager() 156 | { 157 | return $this->manager; 158 | } 159 | 160 | /** 161 | * Get session storage object 162 | * 163 | * Proxies to ManagerInterface::getStorage() 164 | * 165 | * @return Storage 166 | */ 167 | protected function getStorage() 168 | { 169 | return $this->getManager()->getStorage(); 170 | } 171 | 172 | /** 173 | * Create a new container object on which to act 174 | * 175 | * @return ArrayObject 176 | */ 177 | protected function createContainer() 178 | { 179 | return new ArrayObject([], ArrayObject::ARRAY_AS_PROPS); 180 | } 181 | 182 | /** 183 | * Verify container namespace 184 | * 185 | * Checks to see if a container exists within the Storage object already. 186 | * If not, one is created; if so, checks to see if it's an ArrayObject. 187 | * If not, it raises an exception; otherwise, it returns the Storage 188 | * object. 189 | * 190 | * @param bool $createContainer Whether or not to create the container for the namespace 191 | * @return Storage|null Returns null only if $createContainer is false 192 | * @throws Exception\RuntimeException 193 | */ 194 | protected function verifyNamespace($createContainer = true) 195 | { 196 | $storage = $this->getStorage(); 197 | $name = $this->getName(); 198 | if (! isset($storage[$name])) { 199 | if (! $createContainer) { 200 | return; 201 | } 202 | $storage[$name] = $this->createContainer(); 203 | } 204 | if (! is_array($storage[$name]) && ! $storage[$name] instanceof Traversable) { 205 | throw new Exception\RuntimeException('Container cannot write to storage due to type mismatch'); 206 | } 207 | 208 | return $storage; 209 | } 210 | 211 | /** 212 | * Determine whether a given key needs to be expired 213 | * 214 | * Returns true if the key has expired, false otherwise. 215 | * 216 | * @param null|string $key 217 | * @return bool 218 | */ 219 | protected function expireKeys($key = null) 220 | { 221 | $storage = $this->verifyNamespace(); 222 | $name = $this->getName(); 223 | 224 | // Return early if key not found 225 | if ((null !== $key) && ! isset($storage[$name][$key])) { 226 | return true; 227 | } 228 | 229 | if ($this->expireByExpiryTime($storage, $name, $key)) { 230 | return true; 231 | } 232 | 233 | if ($this->expireByHops($storage, $name, $key)) { 234 | return true; 235 | } 236 | 237 | return false; 238 | } 239 | 240 | /** 241 | * Expire a key by expiry time 242 | * 243 | * Checks to see if the entire container has expired based on TTL setting, 244 | * or the individual key. 245 | * 246 | * @param Storage $storage 247 | * @param string $name Container name 248 | * @param string $key Key in container to check 249 | * @return bool 250 | */ 251 | protected function expireByExpiryTime(Storage $storage, $name, $key) 252 | { 253 | $metadata = $storage->getMetadata($name); 254 | 255 | // Global container expiry 256 | if (is_array($metadata) 257 | && isset($metadata['EXPIRE']) 258 | && ($_SERVER['REQUEST_TIME'] > $metadata['EXPIRE']) 259 | ) { 260 | unset($metadata['EXPIRE']); 261 | $storage->setMetadata($name, $metadata, true); 262 | $storage[$name] = $this->createContainer(); 263 | 264 | return true; 265 | } 266 | 267 | // Expire individual key 268 | if ((null !== $key) 269 | && is_array($metadata) 270 | && isset($metadata['EXPIRE_KEYS']) 271 | && isset($metadata['EXPIRE_KEYS'][$key]) 272 | && ($_SERVER['REQUEST_TIME'] > $metadata['EXPIRE_KEYS'][$key]) 273 | ) { 274 | unset($metadata['EXPIRE_KEYS'][$key]); 275 | $storage->setMetadata($name, $metadata, true); 276 | unset($storage[$name][$key]); 277 | 278 | return true; 279 | } 280 | 281 | // Find any keys that have expired 282 | if ((null === $key) 283 | && is_array($metadata) 284 | && isset($metadata['EXPIRE_KEYS']) 285 | ) { 286 | foreach (array_keys($metadata['EXPIRE_KEYS']) as $key) { 287 | if ($_SERVER['REQUEST_TIME'] > $metadata['EXPIRE_KEYS'][$key]) { 288 | unset($metadata['EXPIRE_KEYS'][$key]); 289 | if (isset($storage[$name][$key])) { 290 | unset($storage[$name][$key]); 291 | } 292 | } 293 | } 294 | $storage->setMetadata($name, $metadata, true); 295 | 296 | return true; 297 | } 298 | 299 | return false; 300 | } 301 | 302 | /** 303 | * Expire key by session hops 304 | * 305 | * Determines whether the container or an individual key within it has 306 | * expired based on session hops 307 | * 308 | * @param Storage $storage 309 | * @param string $name 310 | * @param string $key 311 | * @return bool 312 | */ 313 | protected function expireByHops(Storage $storage, $name, $key) 314 | { 315 | $ts = $storage->getRequestAccessTime(); 316 | $metadata = $storage->getMetadata($name); 317 | 318 | // Global container expiry 319 | if (is_array($metadata) 320 | && isset($metadata['EXPIRE_HOPS']) 321 | && ($ts > $metadata['EXPIRE_HOPS']['ts']) 322 | ) { 323 | $metadata['EXPIRE_HOPS']['hops']--; 324 | if (-1 === $metadata['EXPIRE_HOPS']['hops']) { 325 | unset($metadata['EXPIRE_HOPS']); 326 | $storage->setMetadata($name, $metadata, true); 327 | $storage[$name] = $this->createContainer(); 328 | 329 | return true; 330 | } 331 | $metadata['EXPIRE_HOPS']['ts'] = $ts; 332 | $storage->setMetadata($name, $metadata, true); 333 | 334 | return false; 335 | } 336 | 337 | // Single key expiry 338 | if ((null !== $key) 339 | && is_array($metadata) 340 | && isset($metadata['EXPIRE_HOPS_KEYS']) 341 | && isset($metadata['EXPIRE_HOPS_KEYS'][$key]) 342 | && ($ts > $metadata['EXPIRE_HOPS_KEYS'][$key]['ts']) 343 | ) { 344 | $metadata['EXPIRE_HOPS_KEYS'][$key]['hops']--; 345 | if (-1 === $metadata['EXPIRE_HOPS_KEYS'][$key]['hops']) { 346 | unset($metadata['EXPIRE_HOPS_KEYS'][$key]); 347 | $storage->setMetadata($name, $metadata, true); 348 | unset($storage[$name][$key]); 349 | 350 | return true; 351 | } 352 | $metadata['EXPIRE_HOPS_KEYS'][$key]['ts'] = $ts; 353 | $storage->setMetadata($name, $metadata, true); 354 | 355 | return false; 356 | } 357 | 358 | // Find all expired keys 359 | if ((null === $key) 360 | && is_array($metadata) 361 | && isset($metadata['EXPIRE_HOPS_KEYS']) 362 | ) { 363 | foreach (array_keys($metadata['EXPIRE_HOPS_KEYS']) as $key) { 364 | if ($ts > $metadata['EXPIRE_HOPS_KEYS'][$key]['ts']) { 365 | $metadata['EXPIRE_HOPS_KEYS'][$key]['hops']--; 366 | if (-1 === $metadata['EXPIRE_HOPS_KEYS'][$key]['hops']) { 367 | unset($metadata['EXPIRE_HOPS_KEYS'][$key]); 368 | $storage->setMetadata($name, $metadata, true); 369 | unset($storage[$name][$key]); 370 | continue; 371 | } 372 | $metadata['EXPIRE_HOPS_KEYS'][$key]['ts'] = $ts; 373 | } 374 | } 375 | $storage->setMetadata($name, $metadata, true); 376 | 377 | return false; 378 | } 379 | 380 | return false; 381 | } 382 | 383 | /** 384 | * Store a value within the container 385 | * 386 | * @param string $key 387 | * @param mixed $value 388 | * @return void 389 | */ 390 | public function offsetSet($key, $value) 391 | { 392 | $this->expireKeys($key); 393 | $storage = $this->verifyNamespace(); 394 | $name = $this->getName(); 395 | $storage[$name][$key] = $value; 396 | } 397 | 398 | /** 399 | * Determine if the key exists 400 | * 401 | * @param string $key 402 | * @return bool 403 | */ 404 | public function offsetExists($key) 405 | { 406 | // If no container exists, we can't inspect it 407 | if (null === ($storage = $this->verifyNamespace(false))) { 408 | return false; 409 | } 410 | $name = $this->getName(); 411 | 412 | // Return early if the key isn't set 413 | if (! isset($storage[$name][$key])) { 414 | return false; 415 | } 416 | 417 | $expired = $this->expireKeys($key); 418 | 419 | return ! $expired; 420 | } 421 | 422 | /** 423 | * Retrieve a specific key in the container 424 | * 425 | * @param string $key 426 | * @return mixed 427 | */ 428 | public function &offsetGet($key) 429 | { 430 | if (! $this->offsetExists($key)) { 431 | return $this->defaultValue; 432 | } 433 | $storage = $this->getStorage(); 434 | $name = $this->getName(); 435 | 436 | return $storage[$name][$key]; 437 | } 438 | 439 | /** 440 | * Unset a single key in the container 441 | * 442 | * @param string $key 443 | * @return void 444 | */ 445 | public function offsetUnset($key) 446 | { 447 | if (! $this->offsetExists($key)) { 448 | return; 449 | } 450 | $storage = $this->getStorage(); 451 | $name = $this->getName(); 452 | unset($storage[$name][$key]); 453 | } 454 | 455 | /** 456 | * Exchange the current array with another array or object. 457 | * 458 | * @param array|object $input 459 | * @return array Returns the old array 460 | * @see ArrayObject::exchangeArray() 461 | */ 462 | public function exchangeArray($input) 463 | { 464 | // handle arrayobject, iterators and the like: 465 | if (is_object($input) && ($input instanceof ArrayObject || $input instanceof \ArrayObject)) { 466 | $input = $input->getArrayCopy(); 467 | } 468 | if (! is_array($input)) { 469 | $input = (array) $input; 470 | } 471 | 472 | $storage = $this->verifyNamespace(); 473 | $name = $this->getName(); 474 | 475 | $old = $storage[$name]; 476 | $storage[$name] = $input; 477 | if ($old instanceof ArrayObject) { 478 | return $old->getArrayCopy(); 479 | } 480 | 481 | return $old; 482 | } 483 | 484 | /** 485 | * Iterate over session container 486 | * 487 | * @return Iterator 488 | */ 489 | public function getIterator() 490 | { 491 | $this->expireKeys(); 492 | $storage = $this->getStorage(); 493 | $container = $storage[$this->getName()]; 494 | 495 | if ($container instanceof Traversable) { 496 | return $container; 497 | } 498 | 499 | return new ArrayIterator($container); 500 | } 501 | 502 | /** 503 | * Set expiration TTL 504 | * 505 | * Set the TTL for the entire container, a single key, or a set of keys. 506 | * 507 | * @param int $ttl TTL in seconds 508 | * @param string|array|null $vars 509 | * @return Container 510 | * @throws Exception\InvalidArgumentException 511 | */ 512 | public function setExpirationSeconds($ttl, $vars = null) 513 | { 514 | $storage = $this->getStorage(); 515 | $ts = time() + $ttl; 516 | if (is_scalar($vars) && null !== $vars) { 517 | $vars = (array) $vars; 518 | } 519 | 520 | if (null === $vars) { 521 | $this->expireKeys(); // first we need to expire global key, since it can already be expired 522 | $data = ['EXPIRE' => $ts]; 523 | } elseif (is_array($vars)) { 524 | // Cannot pass "$this" to a lambda 525 | $container = $this; 526 | 527 | // Filter out any items not in our container 528 | $expires = array_filter($vars, function ($value) use ($container) { 529 | return $container->offsetExists($value); 530 | }); 531 | 532 | // Map item keys => timestamp 533 | $expires = array_flip($expires); 534 | $expires = array_map(function () use ($ts) { 535 | return $ts; 536 | }, $expires); 537 | 538 | // Create metadata array to merge in 539 | $data = ['EXPIRE_KEYS' => $expires]; 540 | } else { 541 | throw new Exception\InvalidArgumentException( 542 | 'Unknown data provided as second argument to ' . __METHOD__ 543 | ); 544 | } 545 | 546 | $storage->setMetadata( 547 | $this->getName(), 548 | $data 549 | ); 550 | 551 | return $this; 552 | } 553 | 554 | /** 555 | * Set expiration hops for the container, a single key, or set of keys 556 | * 557 | * @param int $hops 558 | * @param null|string|array $vars 559 | * @throws Exception\InvalidArgumentException 560 | * @return Container 561 | */ 562 | public function setExpirationHops($hops, $vars = null) 563 | { 564 | $storage = $this->getStorage(); 565 | $ts = $storage->getRequestAccessTime(); 566 | 567 | if (is_scalar($vars) && (null !== $vars)) { 568 | $vars = (array) $vars; 569 | } 570 | 571 | if (null === $vars) { 572 | $this->expireKeys(); // first we need to expire global key, since it can already be expired 573 | $data = ['EXPIRE_HOPS' => ['hops' => $hops, 'ts' => $ts]]; 574 | } elseif (is_array($vars)) { 575 | // Cannot pass "$this" to a lambda 576 | $container = $this; 577 | 578 | // FilterInterface out any items not in our container 579 | $expires = array_filter($vars, function ($value) use ($container) { 580 | return $container->offsetExists($value); 581 | }); 582 | 583 | // Map item keys => timestamp 584 | $expires = array_flip($expires); 585 | $expires = array_map(function () use ($hops, $ts) { 586 | return ['hops' => $hops, 'ts' => $ts]; 587 | }, $expires); 588 | 589 | // Create metadata array to merge in 590 | $data = ['EXPIRE_HOPS_KEYS' => $expires]; 591 | } else { 592 | throw new Exception\InvalidArgumentException( 593 | 'Unknown data provided as second argument to ' . __METHOD__ 594 | ); 595 | } 596 | 597 | $storage->setMetadata( 598 | $this->getName(), 599 | $data 600 | ); 601 | 602 | return $this; 603 | } 604 | 605 | /** 606 | * Creates a copy of the specific container name 607 | * 608 | * @return array 609 | */ 610 | public function getArrayCopy() 611 | { 612 | $storage = $this->verifyNamespace(); 613 | $container = $storage[$this->getName()]; 614 | 615 | return $container instanceof ArrayObject ? $container->getArrayCopy() : $container; 616 | } 617 | } 618 | -------------------------------------------------------------------------------- /src/AbstractManager.php: -------------------------------------------------------------------------------- 1 | defaultConfigClass)) { 72 | throw new Exception\RuntimeException(sprintf( 73 | 'Unable to locate config class "%s"; class does not exist', 74 | $this->defaultConfigClass 75 | )); 76 | } 77 | 78 | $config = new $this->defaultConfigClass(); 79 | 80 | if (! $config instanceof Config) { 81 | throw new Exception\RuntimeException(sprintf( 82 | 'Default config class %s is invalid; must implement %s\Config\ConfigInterface', 83 | $this->defaultConfigClass, 84 | __NAMESPACE__ 85 | )); 86 | } 87 | } 88 | 89 | $this->config = $config; 90 | 91 | // init storage 92 | if ($storage === null) { 93 | if (! class_exists($this->defaultStorageClass)) { 94 | throw new Exception\RuntimeException(sprintf( 95 | 'Unable to locate storage class "%s"; class does not exist', 96 | $this->defaultStorageClass 97 | )); 98 | } 99 | 100 | $storage = new $this->defaultStorageClass(); 101 | 102 | if (! $storage instanceof Storage) { 103 | throw new Exception\RuntimeException(sprintf( 104 | 'Default storage class %s is invalid; must implement %s\Storage\StorageInterface', 105 | $this->defaultConfigClass, 106 | __NAMESPACE__ 107 | )); 108 | } 109 | } 110 | 111 | $this->storage = $storage; 112 | 113 | // save handler 114 | if ($saveHandler !== null) { 115 | $this->saveHandler = $saveHandler; 116 | } 117 | 118 | $this->validators = $validators; 119 | } 120 | 121 | /** 122 | * Set configuration object 123 | * 124 | * @param Config $config 125 | * @return AbstractManager 126 | */ 127 | public function setConfig(Config $config) 128 | { 129 | $this->config = $config; 130 | return $this; 131 | } 132 | 133 | /** 134 | * Retrieve configuration object 135 | * 136 | * @return Config 137 | */ 138 | public function getConfig() 139 | { 140 | return $this->config; 141 | } 142 | 143 | /** 144 | * Set session storage object 145 | * 146 | * @param Storage $storage 147 | * @return AbstractManager 148 | */ 149 | public function setStorage(Storage $storage) 150 | { 151 | $this->storage = $storage; 152 | return $this; 153 | } 154 | 155 | /** 156 | * Retrieve storage object 157 | * 158 | * @return Storage 159 | */ 160 | public function getStorage() 161 | { 162 | return $this->storage; 163 | } 164 | 165 | /** 166 | * Set session save handler object 167 | * 168 | * @param SaveHandler $saveHandler 169 | * @return AbstractManager 170 | */ 171 | public function setSaveHandler(SaveHandler $saveHandler) 172 | { 173 | $this->saveHandler = $saveHandler; 174 | return $this; 175 | } 176 | 177 | /** 178 | * Get SaveHandler Object 179 | * 180 | * @return SaveHandler 181 | */ 182 | public function getSaveHandler() 183 | { 184 | return $this->saveHandler; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Config/ConfigInterface.php: -------------------------------------------------------------------------------- 1 | setPhpSaveHandler($value); 102 | return $this; 103 | default: 104 | return parent::setOption($option, $value); 105 | } 106 | } 107 | 108 | /** 109 | * Set storage option in backend configuration store 110 | * 111 | * @param string $storageName 112 | * @param mixed $storageValue 113 | * @return SessionConfig 114 | * @throws Exception\InvalidArgumentException 115 | */ 116 | public function setStorageOption($storageName, $storageValue) 117 | { 118 | switch ($storageName) { 119 | case 'remember_me_seconds': 120 | // do nothing; not an INI option 121 | return; 122 | case 'url_rewriter_tags': 123 | $key = 'url_rewriter.tags'; 124 | break; 125 | case 'save_handler': 126 | // Save handlers must be treated differently due to changes 127 | // introduced in PHP 7.2. Do not alter running INI setting. 128 | return $this; 129 | default: 130 | $key = 'session.' . $storageName; 131 | break; 132 | } 133 | 134 | $iniGet = ini_get($key); 135 | $storageValue = (string) $storageValue; 136 | if (false !== $iniGet && (string) $iniGet === $storageValue) { 137 | return $this; 138 | } 139 | 140 | $sessionRequiresRestart = false; 141 | if (session_status() == PHP_SESSION_ACTIVE) { 142 | session_write_close(); 143 | $sessionRequiresRestart = true; 144 | } 145 | 146 | $result = ini_set($key, $storageValue); 147 | 148 | if ($sessionRequiresRestart) { 149 | session_start(); 150 | } 151 | 152 | if (false === $result) { 153 | throw new Exception\InvalidArgumentException( 154 | "'{$key}' is not a valid sessions-related ini setting." 155 | ); 156 | } 157 | return $this; 158 | } 159 | 160 | /** 161 | * Retrieve a storage option from a backend configuration store 162 | * 163 | * Used to retrieve default values from a backend configuration store. 164 | * 165 | * @param string $storageOption 166 | * @return mixed 167 | */ 168 | public function getStorageOption($storageOption) 169 | { 170 | switch ($storageOption) { 171 | case 'remember_me_seconds': 172 | // No remote storage option; just return the current value 173 | return $this->rememberMeSeconds; 174 | case 'url_rewriter_tags': 175 | return ini_get('url_rewriter.tags'); 176 | // The following all need a transformation on the retrieved value; 177 | // however they use the same key naming scheme 178 | case 'use_cookies': 179 | case 'use_only_cookies': 180 | case 'use_trans_sid': 181 | case 'cookie_httponly': 182 | return (bool) ini_get('session.' . $storageOption); 183 | case 'save_handler': 184 | // Save handlers must be treated differently due to changes 185 | // introduced in PHP 7.2. 186 | return $this->saveHandler ?: session_module_name(); 187 | default: 188 | return ini_get('session.' . $storageOption); 189 | } 190 | } 191 | 192 | /** 193 | * Proxy to setPhpSaveHandler() 194 | * 195 | * Prevents calls to `setSaveHandler()` from hitting `setOption()` instead, 196 | * and thus bypassing the logic of `setPhpSaveHandler()`. 197 | * 198 | * @param string $phpSaveHandler 199 | * @return SessionConfig 200 | * @throws Exception\InvalidArgumentException 201 | */ 202 | public function setSaveHandler($phpSaveHandler) 203 | { 204 | return $this->setPhpSaveHandler($phpSaveHandler); 205 | } 206 | 207 | /** 208 | * Set session.save_handler 209 | * 210 | * @param string $phpSaveHandler 211 | * @return SessionConfig 212 | * @throws Exception\InvalidArgumentException 213 | */ 214 | public function setPhpSaveHandler($phpSaveHandler) 215 | { 216 | $this->saveHandler = $this->performSaveHandlerUpdate($phpSaveHandler); 217 | $this->options['save_handler'] = $this->saveHandler; 218 | return $this; 219 | } 220 | 221 | /** 222 | * Set session.save_path 223 | * 224 | * @param string $savePath 225 | * @return SessionConfig 226 | * @throws Exception\InvalidArgumentException on invalid path 227 | */ 228 | public function setSavePath($savePath) 229 | { 230 | if ($this->getOption('save_handler') === 'files') { 231 | parent::setSavePath($savePath); 232 | } 233 | $this->savePath = $savePath; 234 | $this->setOption('save_path', $savePath); 235 | return $this; 236 | } 237 | 238 | /** 239 | * Set session.serialize_handler 240 | * 241 | * @param string $serializeHandler 242 | * @return SessionConfig 243 | * @throws Exception\InvalidArgumentException 244 | */ 245 | public function setSerializeHandler($serializeHandler) 246 | { 247 | $serializeHandler = (string) $serializeHandler; 248 | 249 | set_error_handler([$this, 'handleError']); 250 | ini_set('session.serialize_handler', $serializeHandler); 251 | restore_error_handler(); 252 | if ($this->phpErrorCode >= E_WARNING) { 253 | throw new Exception\InvalidArgumentException('Invalid serialize handler specified'); 254 | } 255 | 256 | $this->serializeHandler = (string) $serializeHandler; 257 | return $this; 258 | } 259 | 260 | // session.cache_limiter 261 | 262 | /** 263 | * Set cache limiter 264 | * 265 | * @param $cacheLimiter 266 | * @return SessionConfig 267 | * @throws Exception\InvalidArgumentException 268 | */ 269 | public function setCacheLimiter($cacheLimiter) 270 | { 271 | $cacheLimiter = (string) $cacheLimiter; 272 | if (! in_array($cacheLimiter, $this->validCacheLimiters)) { 273 | throw new Exception\InvalidArgumentException('Invalid cache limiter provided'); 274 | } 275 | $this->setOption('cache_limiter', $cacheLimiter); 276 | ini_set('session.cache_limiter', $cacheLimiter); 277 | return $this; 278 | } 279 | 280 | /** 281 | * Set session.hash_function 282 | * 283 | * @param string|int $hashFunction 284 | * @return SessionConfig 285 | * @throws Exception\InvalidArgumentException 286 | */ 287 | public function setHashFunction($hashFunction) 288 | { 289 | if (PHP_VERSION_ID >= 70100) { 290 | trigger_error('session.hash_function is removed starting with PHP 7.1', E_USER_DEPRECATED); 291 | } 292 | 293 | $hashFunction = (string) $hashFunction; 294 | $validHashFunctions = $this->getHashFunctions(); 295 | if (! in_array($hashFunction, $validHashFunctions, true)) { 296 | throw new Exception\InvalidArgumentException('Invalid hash function provided'); 297 | } 298 | 299 | $this->setOption('hash_function', $hashFunction); 300 | ini_set('session.hash_function', $hashFunction); 301 | return $this; 302 | } 303 | 304 | /** 305 | * Set session.hash_bits_per_character 306 | * 307 | * @param int $hashBitsPerCharacter 308 | * @return SessionConfig 309 | * @throws Exception\InvalidArgumentException 310 | */ 311 | public function setHashBitsPerCharacter($hashBitsPerCharacter) 312 | { 313 | if (PHP_VERSION_ID >= 70100) { 314 | trigger_error('session.hash_bits_per_character is removed starting with PHP 7.1', E_USER_DEPRECATED); 315 | } 316 | 317 | if (! is_numeric($hashBitsPerCharacter) 318 | || ! in_array($hashBitsPerCharacter, $this->validHashBitsPerCharacters) 319 | ) { 320 | throw new Exception\InvalidArgumentException('Invalid hash bits per character provided'); 321 | } 322 | 323 | $hashBitsPerCharacter = (int) $hashBitsPerCharacter; 324 | $this->setOption('hash_bits_per_character', $hashBitsPerCharacter); 325 | ini_set('session.hash_bits_per_character', $hashBitsPerCharacter); 326 | return $this; 327 | } 328 | 329 | /** 330 | * Set session.sid_bits_per_character 331 | * 332 | * @param int $sidBitsPerCharacter 333 | * @return SessionConfig 334 | * @throws Exception\InvalidArgumentException 335 | */ 336 | public function setSidBitsPerCharacter($sidBitsPerCharacter) 337 | { 338 | if (! is_numeric($sidBitsPerCharacter) 339 | || ! in_array($sidBitsPerCharacter, $this->validSidBitsPerCharacters) 340 | ) { 341 | throw new Exception\InvalidArgumentException('Invalid sid bits per character provided'); 342 | } 343 | 344 | $sidBitsPerCharacter = (int) $sidBitsPerCharacter; 345 | $this->setOption('sid_bits_per_character', $sidBitsPerCharacter); 346 | ini_set('session.sid_bits_per_character', $sidBitsPerCharacter); 347 | return $this; 348 | } 349 | 350 | /** 351 | * Retrieve list of valid hash functions 352 | * 353 | * @return array 354 | */ 355 | protected function getHashFunctions() 356 | { 357 | if (empty($this->validHashFunctions)) { 358 | /** 359 | * @link http://php.net/manual/en/session.configuration.php#ini.session.hash-function 360 | * "0" and "1" refer to MD5-128 and SHA1-160, respectively, and are 361 | * valid in addition to whatever is reported by hash_algos() 362 | */ 363 | $this->validHashFunctions = array_merge(['0', '1'], hash_algos()); 364 | } 365 | return $this->validHashFunctions; 366 | } 367 | 368 | /** 369 | * Handle PHP errors 370 | * 371 | * @param int $code 372 | * @param string $message 373 | * @return void 374 | */ 375 | protected function handleError($code, $message) 376 | { 377 | $this->phpErrorCode = $code; 378 | $this->phpErrorMessage = $message; 379 | } 380 | 381 | /** 382 | * Determine what save handlers are available. 383 | * 384 | * The only way to get at this information is via phpinfo(), and the output 385 | * of that function varies based on the SAPI. 386 | * 387 | * Strips the handler "user" from the list, as PHP 7.2 does not allow 388 | * setting that as a handler, because it essentially requires you to have 389 | * already set a custom handler via `session_set_save_handler()`. It 390 | * wasn't really valid in prior versions, either; the language simply did 391 | * not complain previously. 392 | * 393 | * @return array 394 | */ 395 | private function locateRegisteredSaveHandlers() 396 | { 397 | if (null !== $this->knownSaveHandlers) { 398 | return $this->knownSaveHandlers; 399 | } 400 | 401 | if (! preg_match('#Registered save handlers.*#m', $this->getPhpInfoForModules(), $matches)) { 402 | $this->knownSaveHandlers = []; 403 | return $this->knownSaveHandlers; 404 | } 405 | 406 | $content = array_shift($matches); 407 | 408 | $handlers = false !== strpos($content, '') 409 | ? $this->parseSaveHandlersFromHtml($content) 410 | : $this->parseSaveHandlersFromPlainText($content); 411 | 412 | if (false !== ($index = array_search('user', $handlers, true))) { 413 | unset($handlers[$index]); 414 | } 415 | 416 | $this->knownSaveHandlers = $handlers; 417 | 418 | return $this->knownSaveHandlers; 419 | } 420 | 421 | /** 422 | * Perform a session.save_handler update. 423 | * 424 | * Determines if the save handler represents a PHP built-in 425 | * save handler, and, if so, passes that value to session_module_name 426 | * in order to activate it. The save handler name is then returned. 427 | * 428 | * If it is not, it tests to see if it is a SessionHandlerInterface 429 | * implementation. If the string is a class implementing that interface, 430 | * it creates an instance of it. In such cases, it then calls 431 | * session_set_save_handler to activate it. The class name of the 432 | * handler is returned. 433 | * 434 | * In all other cases, an exception is raised. 435 | * 436 | * @param string|SessionHandlerInterface $phpSaveHandler 437 | * @return string 438 | * @throws Exception\InvalidArgumentException if an error occurs when 439 | * setting a PHP session save handler module. 440 | * @throws Exception\InvalidArgumentException if the $phpSaveHandler 441 | * is a string that does not represent a class implementing 442 | * SessionHandlerInterface. 443 | * @throws Exception\InvalidArgumentException if $phpSaveHandler is 444 | * a non-string value that does not implement SessionHandlerInterface. 445 | */ 446 | private function performSaveHandlerUpdate($phpSaveHandler) 447 | { 448 | if (is_string($phpSaveHandler)) { 449 | $knownHandlers = $this->locateRegisteredSaveHandlers(); 450 | 451 | if (in_array($phpSaveHandler, $knownHandlers, true)) { 452 | $phpSaveHandler = strtolower($phpSaveHandler); 453 | set_error_handler([$this, 'handleError']); 454 | session_module_name($phpSaveHandler); 455 | restore_error_handler(); 456 | if ($this->phpErrorCode >= E_WARNING) { 457 | throw new Exception\InvalidArgumentException(sprintf( 458 | 'Error setting session save handler module "%s": %s', 459 | $phpSaveHandler, 460 | $this->phpErrorMessage 461 | )); 462 | } 463 | 464 | return $phpSaveHandler; 465 | } 466 | 467 | if (! class_exists($phpSaveHandler) 468 | || ! is_a($phpSaveHandler, SessionHandlerInterface::class, true) 469 | ) { 470 | throw new Exception\InvalidArgumentException(sprintf( 471 | 'Invalid save handler specified ("%s"); must be one of [%s]' 472 | . ' or a class implementing %s', 473 | $phpSaveHandler, 474 | implode(', ', $knownHandlers), 475 | SessionHandlerInterface::class 476 | )); 477 | } 478 | 479 | $phpSaveHandler = new $phpSaveHandler(); 480 | } 481 | 482 | if (! $phpSaveHandler instanceof SessionHandlerInterface) { 483 | throw new Exception\InvalidArgumentException(sprintf( 484 | 'Invalid save handler specified ("%s"); must implement %s', 485 | get_class($phpSaveHandler), 486 | SessionHandlerInterface::class 487 | )); 488 | } 489 | 490 | session_set_save_handler($phpSaveHandler); 491 | 492 | return get_class($phpSaveHandler); 493 | } 494 | 495 | /** 496 | * Grab module information from phpinfo. 497 | * 498 | * Requires capturing an output buffer, as phpinfo does not have an option 499 | * to return the value as a string. 500 | * 501 | * @return string 502 | */ 503 | private function getPhpInfoForModules() 504 | { 505 | ob_start(); 506 | phpinfo(INFO_MODULES); 507 | return ob_get_clean(); 508 | } 509 | 510 | /** 511 | * Parse a list of PHP session save handlers from HTML. 512 | * 513 | * Format is "Registered save handlers{handlers}". 514 | * 515 | * @param string $content 516 | * @return array 517 | */ 518 | private function parseSaveHandlersFromHtml($content) 519 | { 520 | if (! preg_match('#(?P[^<]+)#', $content, $matches)) { 521 | return []; 522 | } 523 | 524 | $handlers = trim($matches['handlers']); 525 | return preg_split('#\s+#', $handlers); 526 | } 527 | 528 | /** 529 | * Parse a list of PHP session save handlers from plain text. 530 | * 531 | * Format is "Registered save handlers => ". 532 | * 533 | * @param string $content 534 | * @return array 535 | */ 536 | private function parseSaveHandlersFromPlainText($content) 537 | { 538 | list($prefix, $handlers) = explode('=>', $content); 539 | $handlers = trim($handlers); 540 | return preg_split('#\s+#', $handlers); 541 | } 542 | } 543 | -------------------------------------------------------------------------------- /src/Config/StandardConfig.php: -------------------------------------------------------------------------------- 1 | $value) { 110 | $setter = 'set' . str_replace(' ', '', ucwords(str_replace('_', ' ', $key))); 111 | if (method_exists($this, $setter)) { 112 | $this->{$setter}($value); 113 | } else { 114 | $this->setOption($key, $value); 115 | } 116 | } 117 | return $this; 118 | } 119 | 120 | /** 121 | * Get all options set 122 | * 123 | * @return array 124 | */ 125 | public function getOptions() 126 | { 127 | return $this->options; 128 | } 129 | 130 | /** 131 | * Set an individual option 132 | * 133 | * Keys are normalized to lowercase. After setting internally, calls 134 | * {@link setStorageOption()} to allow further processing. 135 | * 136 | * 137 | * @param string $option 138 | * @param mixed $value 139 | * @return StandardConfig 140 | */ 141 | public function setOption($option, $value) 142 | { 143 | $option = strtolower($option); 144 | $this->options[$option] = $value; 145 | $this->setStorageOption($option, $value); 146 | return $this; 147 | } 148 | 149 | /** 150 | * Get an individual option 151 | * 152 | * Keys are normalized to lowercase. If the option is not found, attempts 153 | * to retrieve it via {@link getStorageOption()}; if a value is returned 154 | * from that method, it will be set as the internal value and returned. 155 | * 156 | * Returns null for unfound options 157 | * 158 | * @param string $option 159 | * @return mixed 160 | */ 161 | public function getOption($option) 162 | { 163 | $option = strtolower($option); 164 | if (array_key_exists($option, $this->options)) { 165 | return $this->options[$option]; 166 | } 167 | 168 | $value = $this->getStorageOption($option); 169 | if (null !== $value) { 170 | $this->setOption($option, $value); 171 | return $value; 172 | } 173 | 174 | return; 175 | } 176 | 177 | /** 178 | * Check to see if an internal option has been set for the key provided. 179 | * 180 | * @param string $option 181 | * @return bool 182 | */ 183 | public function hasOption($option) 184 | { 185 | $option = strtolower($option); 186 | return array_key_exists($option, $this->options); 187 | } 188 | 189 | /** 190 | * Set storage option in backend configuration store 191 | * 192 | * Does nothing in this implementation; others might use it to set things 193 | * such as INI settings. 194 | * 195 | * @param string $storageName 196 | * @param mixed $storageValue 197 | * @return StandardConfig 198 | */ 199 | public function setStorageOption($storageName, $storageValue) 200 | { 201 | return $this; 202 | } 203 | 204 | /** 205 | * Retrieve a storage option from a backend configuration store 206 | * 207 | * Used to retrieve default values from a backend configuration store. 208 | * 209 | * @param string $storageOption 210 | * @return mixed 211 | */ 212 | public function getStorageOption($storageOption) 213 | { 214 | return; 215 | } 216 | 217 | /** 218 | * Set session.save_path 219 | * 220 | * @param string $savePath 221 | * @return StandardConfig 222 | * @throws Exception\InvalidArgumentException on invalid path 223 | */ 224 | public function setSavePath($savePath) 225 | { 226 | if (! is_dir($savePath)) { 227 | throw new Exception\InvalidArgumentException('Invalid save_path provided; not a directory'); 228 | } 229 | if (! is_writable($savePath)) { 230 | throw new Exception\InvalidArgumentException('Invalid save_path provided; not writable'); 231 | } 232 | 233 | $this->savePath = $savePath; 234 | $this->setStorageOption('save_path', $savePath); 235 | return $this; 236 | } 237 | 238 | /** 239 | * Set session.save_path 240 | * 241 | * @return string|null 242 | */ 243 | public function getSavePath() 244 | { 245 | if (null === $this->savePath) { 246 | $this->savePath = $this->getStorageOption('save_path'); 247 | } 248 | return $this->savePath; 249 | } 250 | 251 | /** 252 | * Set session.name 253 | * 254 | * @param string $name 255 | * @return StandardConfig 256 | * @throws Exception\InvalidArgumentException 257 | */ 258 | public function setName($name) 259 | { 260 | $this->name = (string) $name; 261 | if (empty($this->name)) { 262 | throw new Exception\InvalidArgumentException('Invalid session name; cannot be empty'); 263 | } 264 | $this->setStorageOption('name', $this->name); 265 | return $this; 266 | } 267 | 268 | /** 269 | * Get session.name 270 | * 271 | * @return null|string 272 | */ 273 | public function getName() 274 | { 275 | if (null === $this->name) { 276 | $this->name = $this->getStorageOption('name'); 277 | } 278 | return $this->name; 279 | } 280 | 281 | /** 282 | * Set session.gc_probability 283 | * 284 | * @param int $gcProbability 285 | * @return StandardConfig 286 | * @throws Exception\InvalidArgumentException 287 | */ 288 | public function setGcProbability($gcProbability) 289 | { 290 | if (! is_numeric($gcProbability)) { 291 | throw new Exception\InvalidArgumentException('Invalid gc_probability; must be numeric'); 292 | } 293 | $gcProbability = (int) $gcProbability; 294 | if (0 > $gcProbability || 100 < $gcProbability) { 295 | throw new Exception\InvalidArgumentException('Invalid gc_probability; must be a percentage'); 296 | } 297 | $this->setOption('gc_probability', $gcProbability); 298 | $this->setStorageOption('gc_probability', $gcProbability); 299 | return $this; 300 | } 301 | 302 | /** 303 | * Get session.gc_probability 304 | * 305 | * @return int 306 | */ 307 | public function getGcProbability() 308 | { 309 | if (! isset($this->options['gc_probability'])) { 310 | $this->options['gc_probability'] = $this->getStorageOption('gc_probability'); 311 | } 312 | 313 | return $this->options['gc_probability']; 314 | } 315 | 316 | /** 317 | * Set session.gc_divisor 318 | * 319 | * @param int $gcDivisor 320 | * @return StandardConfig 321 | * @throws Exception\InvalidArgumentException 322 | */ 323 | public function setGcDivisor($gcDivisor) 324 | { 325 | if (! is_numeric($gcDivisor)) { 326 | throw new Exception\InvalidArgumentException('Invalid gc_divisor; must be numeric'); 327 | } 328 | $gcDivisor = (int) $gcDivisor; 329 | if (1 > $gcDivisor) { 330 | throw new Exception\InvalidArgumentException('Invalid gc_divisor; must be a positive integer'); 331 | } 332 | $this->setOption('gc_divisor', $gcDivisor); 333 | $this->setStorageOption('gc_divisor', $gcDivisor); 334 | return $this; 335 | } 336 | 337 | /** 338 | * Get session.gc_divisor 339 | * 340 | * @return int 341 | */ 342 | public function getGcDivisor() 343 | { 344 | if (! isset($this->options['gc_divisor'])) { 345 | $this->options['gc_divisor'] = $this->getStorageOption('gc_divisor'); 346 | } 347 | 348 | return $this->options['gc_divisor']; 349 | } 350 | 351 | /** 352 | * Set gc_maxlifetime 353 | * 354 | * @param int $gcMaxlifetime 355 | * @return StandardConfig 356 | * @throws Exception\InvalidArgumentException 357 | */ 358 | public function setGcMaxlifetime($gcMaxlifetime) 359 | { 360 | if (! is_numeric($gcMaxlifetime)) { 361 | throw new Exception\InvalidArgumentException('Invalid gc_maxlifetime; must be numeric'); 362 | } 363 | 364 | $gcMaxlifetime = (int) $gcMaxlifetime; 365 | if (1 > $gcMaxlifetime) { 366 | throw new Exception\InvalidArgumentException('Invalid gc_maxlifetime; must be a positive integer'); 367 | } 368 | 369 | $this->setOption('gc_maxlifetime', $gcMaxlifetime); 370 | $this->setStorageOption('gc_maxlifetime', $gcMaxlifetime); 371 | return $this; 372 | } 373 | 374 | /** 375 | * Get session.gc_maxlifetime 376 | * 377 | * @return int 378 | */ 379 | public function getGcMaxlifetime() 380 | { 381 | if (! isset($this->options['gc_maxlifetime'])) { 382 | $this->options['gc_maxlifetime'] = $this->getStorageOption('gc_maxlifetime'); 383 | } 384 | 385 | return $this->options['gc_maxlifetime']; 386 | } 387 | 388 | /** 389 | * Set session.cookie_lifetime 390 | * 391 | * @param int $cookieLifetime 392 | * @return StandardConfig 393 | * @throws Exception\InvalidArgumentException 394 | */ 395 | public function setCookieLifetime($cookieLifetime) 396 | { 397 | if (! is_numeric($cookieLifetime)) { 398 | throw new Exception\InvalidArgumentException('Invalid cookie_lifetime; must be numeric'); 399 | } 400 | if (0 > $cookieLifetime) { 401 | throw new Exception\InvalidArgumentException( 402 | 'Invalid cookie_lifetime; must be a positive integer or zero' 403 | ); 404 | } 405 | 406 | $this->cookieLifetime = (int) $cookieLifetime; 407 | $this->setStorageOption('cookie_lifetime', $this->cookieLifetime); 408 | return $this; 409 | } 410 | 411 | /** 412 | * Get session.cookie_lifetime 413 | * 414 | * @return int 415 | */ 416 | public function getCookieLifetime() 417 | { 418 | if (null === $this->cookieLifetime) { 419 | $this->cookieLifetime = $this->getStorageOption('cookie_lifetime'); 420 | } 421 | return $this->cookieLifetime; 422 | } 423 | 424 | /** 425 | * Set session.cookie_path 426 | * 427 | * @param string $cookiePath 428 | * @return StandardConfig 429 | * @throws Exception\InvalidArgumentException 430 | */ 431 | public function setCookiePath($cookiePath) 432 | { 433 | $cookiePath = (string) $cookiePath; 434 | 435 | $test = parse_url($cookiePath, PHP_URL_PATH); 436 | if ($test != $cookiePath || '/' != $test[0]) { 437 | throw new Exception\InvalidArgumentException('Invalid cookie path'); 438 | } 439 | 440 | $this->cookiePath = $cookiePath; 441 | $this->setStorageOption('cookie_path', $cookiePath); 442 | return $this; 443 | } 444 | 445 | /** 446 | * Get session.cookie_path 447 | * 448 | * @return string 449 | */ 450 | public function getCookiePath() 451 | { 452 | if (null === $this->cookiePath) { 453 | $this->cookiePath = $this->getStorageOption('cookie_path'); 454 | } 455 | return $this->cookiePath; 456 | } 457 | 458 | /** 459 | * Set session.cookie_domain 460 | * 461 | * @param string $cookieDomain 462 | * @return StandardConfig 463 | * @throws Exception\InvalidArgumentException 464 | */ 465 | public function setCookieDomain($cookieDomain) 466 | { 467 | if (! is_string($cookieDomain)) { 468 | throw new Exception\InvalidArgumentException('Invalid cookie domain: must be a string'); 469 | } 470 | 471 | $validator = new HostnameValidator(HostnameValidator::ALLOW_ALL); 472 | 473 | if (! empty($cookieDomain) && ! $validator->isValid($cookieDomain)) { 474 | throw new Exception\InvalidArgumentException( 475 | 'Invalid cookie domain: ' . implode('; ', $validator->getMessages()) 476 | ); 477 | } 478 | 479 | $this->cookieDomain = $cookieDomain; 480 | $this->setStorageOption('cookie_domain', $cookieDomain); 481 | return $this; 482 | } 483 | 484 | /** 485 | * Get session.cookie_domain 486 | * 487 | * @return string 488 | */ 489 | public function getCookieDomain() 490 | { 491 | if (null === $this->cookieDomain) { 492 | $this->cookieDomain = $this->getStorageOption('cookie_domain'); 493 | } 494 | return $this->cookieDomain; 495 | } 496 | 497 | /** 498 | * Set session.cookie_secure 499 | * 500 | * @param bool $cookieSecure 501 | * @return StandardConfig 502 | */ 503 | public function setCookieSecure($cookieSecure) 504 | { 505 | $this->cookieSecure = (bool) $cookieSecure; 506 | $this->setStorageOption('cookie_secure', $this->cookieSecure); 507 | return $this; 508 | } 509 | 510 | /** 511 | * Get session.cookie_secure 512 | * 513 | * @return bool 514 | */ 515 | public function getCookieSecure() 516 | { 517 | if (null === $this->cookieSecure) { 518 | $this->cookieSecure = $this->getStorageOption('cookie_secure'); 519 | } 520 | return $this->cookieSecure; 521 | } 522 | 523 | /** 524 | * Set session.cookie_httponly 525 | * 526 | * case sensitive method lookups in setOptions means this method has an 527 | * unusual casing 528 | * 529 | * @param bool $cookieHttpOnly 530 | * @return StandardConfig 531 | */ 532 | public function setCookieHttpOnly($cookieHttpOnly) 533 | { 534 | $this->cookieHttpOnly = (bool) $cookieHttpOnly; 535 | $this->setStorageOption('cookie_httponly', $this->cookieHttpOnly); 536 | return $this; 537 | } 538 | 539 | /** 540 | * Get session.cookie_httponly 541 | * 542 | * @return bool 543 | */ 544 | public function getCookieHttpOnly() 545 | { 546 | if (null === $this->cookieHttpOnly) { 547 | $this->cookieHttpOnly = $this->getStorageOption('cookie_httponly'); 548 | } 549 | return $this->cookieHttpOnly; 550 | } 551 | 552 | /** 553 | * Set session.use_cookies 554 | * 555 | * @param bool $useCookies 556 | * @return StandardConfig 557 | */ 558 | public function setUseCookies($useCookies) 559 | { 560 | $this->useCookies = (bool) $useCookies; 561 | $this->setStorageOption('use_cookies', $this->useCookies); 562 | return $this; 563 | } 564 | 565 | /** 566 | * Get session.use_cookies 567 | * 568 | * @return bool 569 | */ 570 | public function getUseCookies() 571 | { 572 | if (null === $this->useCookies) { 573 | $this->useCookies = $this->getStorageOption('use_cookies'); 574 | } 575 | return $this->useCookies; 576 | } 577 | 578 | /** 579 | * Set session.entropy_file 580 | * 581 | * @param string $entropyFile 582 | * @return StandardConfig 583 | * @throws Exception\InvalidArgumentException 584 | */ 585 | public function setEntropyFile($entropyFile) 586 | { 587 | if (PHP_VERSION_ID >= 70100) { 588 | trigger_error('session.entropy_file is removed starting with PHP 7.1', E_USER_DEPRECATED); 589 | } 590 | 591 | if (! is_readable($entropyFile)) { 592 | throw new Exception\InvalidArgumentException(sprintf( 593 | "Invalid entropy_file provided: '%s'; doesn't exist or not readable", 594 | $entropyFile 595 | )); 596 | } 597 | 598 | $this->setOption('entropy_file', $entropyFile); 599 | $this->setStorageOption('entropy_file', $entropyFile); 600 | return $this; 601 | } 602 | 603 | /** 604 | * Get session.entropy_file 605 | * 606 | * @return string 607 | */ 608 | public function getEntropyFile() 609 | { 610 | if (PHP_VERSION_ID >= 70100) { 611 | trigger_error('session.entropy_file is removed starting with PHP 7.1', E_USER_DEPRECATED); 612 | } 613 | 614 | if (! isset($this->options['entropy_file'])) { 615 | $this->options['entropy_file'] = $this->getStorageOption('entropy_file'); 616 | } 617 | 618 | return $this->options['entropy_file']; 619 | } 620 | 621 | /** 622 | * set session.entropy_length 623 | * 624 | * @param int $entropyLength 625 | * @return StandardConfig 626 | * @throws Exception\InvalidArgumentException 627 | */ 628 | public function setEntropyLength($entropyLength) 629 | { 630 | if (PHP_VERSION_ID >= 70100) { 631 | trigger_error('session.entropy_length is removed starting with PHP 7.1', E_USER_DEPRECATED); 632 | } 633 | 634 | if (! is_numeric($entropyLength)) { 635 | throw new Exception\InvalidArgumentException('Invalid entropy_length; must be numeric'); 636 | } 637 | if (0 > $entropyLength) { 638 | throw new Exception\InvalidArgumentException('Invalid entropy_length; must be a positive integer or zero'); 639 | } 640 | 641 | $this->setOption('entropy_length', $entropyLength); 642 | $this->setStorageOption('entropy_length', $entropyLength); 643 | return $this; 644 | } 645 | 646 | /** 647 | * Get session.entropy_length 648 | * 649 | * @return string 650 | */ 651 | public function getEntropyLength() 652 | { 653 | if (PHP_VERSION_ID >= 70100) { 654 | trigger_error('session.entropy_length is removed starting with PHP 7.1', E_USER_DEPRECATED); 655 | } 656 | 657 | if (! isset($this->options['entropy_length'])) { 658 | $this->options['entropy_length'] = $this->getStorageOption('entropy_length'); 659 | } 660 | 661 | return $this->options['entropy_length']; 662 | } 663 | 664 | /** 665 | * Set session.cache_expire 666 | * 667 | * @param int $cacheExpire 668 | * @return StandardConfig 669 | * @throws Exception\InvalidArgumentException 670 | */ 671 | public function setCacheExpire($cacheExpire) 672 | { 673 | if (! is_numeric($cacheExpire)) { 674 | throw new Exception\InvalidArgumentException('Invalid cache_expire; must be numeric'); 675 | } 676 | 677 | $cacheExpire = (int) $cacheExpire; 678 | if (1 > $cacheExpire) { 679 | throw new Exception\InvalidArgumentException('Invalid cache_expire; must be a positive integer'); 680 | } 681 | 682 | $this->setOption('cache_expire', $cacheExpire); 683 | $this->setStorageOption('cache_expire', $cacheExpire); 684 | return $this; 685 | } 686 | 687 | /** 688 | * Get session.cache_expire 689 | * 690 | * @return string 691 | */ 692 | public function getCacheExpire() 693 | { 694 | if (! isset($this->options['cache_expire'])) { 695 | $this->options['cache_expire'] = $this->getStorageOption('cache_expire'); 696 | } 697 | 698 | return $this->options['cache_expire']; 699 | } 700 | 701 | /** 702 | * Set session.hash_function 703 | * 704 | * @param string $hashFunction 705 | * @return mixed 706 | */ 707 | public function setHashFunction($hashFunction) 708 | { 709 | if (PHP_VERSION_ID >= 70100) { 710 | trigger_error('session.hash_function is removed starting with PHP 7.1', E_USER_DEPRECATED); 711 | } 712 | 713 | return $this->setOption('hash_function', $hashFunction); 714 | } 715 | 716 | /** 717 | * Get session.hash_function 718 | * 719 | * @return string 720 | */ 721 | public function getHashFunction() 722 | { 723 | if (PHP_VERSION_ID >= 70100) { 724 | trigger_error('session.hash_function is removed starting with PHP 7.1', E_USER_DEPRECATED); 725 | } 726 | 727 | return $this->getOption('hash_function'); 728 | } 729 | 730 | /** 731 | * Set session.hash_bits_per_character 732 | * 733 | * @param int $hashBitsPerCharacter 734 | * @return StandardConfig 735 | * @throws Exception\InvalidArgumentException 736 | */ 737 | public function setHashBitsPerCharacter($hashBitsPerCharacter) 738 | { 739 | if (PHP_VERSION_ID >= 70100) { 740 | trigger_error('session.hash_bits_per_character is removed starting with PHP 7.1', E_USER_DEPRECATED); 741 | } 742 | 743 | if (! is_numeric($hashBitsPerCharacter)) { 744 | throw new Exception\InvalidArgumentException('Invalid hash bits per character provided'); 745 | } 746 | $hashBitsPerCharacter = (int) $hashBitsPerCharacter; 747 | $this->setOption('hash_bits_per_character', $hashBitsPerCharacter); 748 | $this->setStorageOption('hash_bits_per_character', $hashBitsPerCharacter); 749 | return $this; 750 | } 751 | 752 | /** 753 | * Get session.hash_bits_per_character 754 | * 755 | * @return string 756 | */ 757 | public function getHashBitsPerCharacter() 758 | { 759 | if (PHP_VERSION_ID >= 70100) { 760 | trigger_error('session.hash_bits_per_character is removed starting with PHP 7.1', E_USER_DEPRECATED); 761 | } 762 | 763 | if (! isset($this->options['hash_bits_per_character'])) { 764 | $this->options['hash_bits_per_character'] = $this->getStorageOption('hash_bits_per_character'); 765 | } 766 | 767 | return $this->options['hash_bits_per_character']; 768 | } 769 | 770 | /** 771 | * Set session.sid_length 772 | * 773 | * @param int $sidLength 774 | * @return StandardConfig 775 | * @throws Exception\InvalidArgumentException 776 | */ 777 | public function setSidLength($sidLength) 778 | { 779 | if (! is_numeric($sidLength) || $sidLength < 22 || $sidLength > 256) { 780 | throw new Exception\InvalidArgumentException('Invalid length provided'); 781 | } 782 | $sidLength = (int) $sidLength; 783 | $this->setOption('sid_length', $sidLength); 784 | $this->setStorageOption('sid_length', $sidLength); 785 | return $this; 786 | } 787 | 788 | /** 789 | * Get session.sid_length 790 | * 791 | * @return string 792 | */ 793 | public function getSidLength() 794 | { 795 | if (! isset($this->options['sid_length'])) { 796 | $this->options['sid_length'] = $this->getStorageOption('sid_length'); 797 | } 798 | 799 | return $this->options['sid_length']; 800 | } 801 | 802 | /** 803 | * Set session.sid_bits_per_character 804 | * 805 | * @param int $sidBitsPerCharacter 806 | * @return StandardConfig 807 | * @throws Exception\InvalidArgumentException 808 | */ 809 | public function setSidBitsPerCharacter($sidBitsPerCharacter) 810 | { 811 | if (! is_numeric($sidBitsPerCharacter)) { 812 | throw new Exception\InvalidArgumentException('Invalid sid bits per character provided'); 813 | } 814 | $sidBitsPerCharacter = (int) $sidBitsPerCharacter; 815 | $this->setOption('sid_bits_per_character', $sidBitsPerCharacter); 816 | $this->setStorageOption('sid_bits_per_character', $sidBitsPerCharacter); 817 | return $this; 818 | } 819 | 820 | /** 821 | * Get session.sid_bits_per_character 822 | * 823 | * @return string 824 | */ 825 | public function getSidBitsPerCharacter() 826 | { 827 | if (! isset($this->options['sid_bits_per_character'])) { 828 | $this->options['sid_bits_per_character'] = $this->getStorageOption('sid_bits_per_character'); 829 | } 830 | 831 | return $this->options['sid_bits_per_character']; 832 | } 833 | 834 | /** 835 | * Set remember_me_seconds 836 | * 837 | * @param int $rememberMeSeconds 838 | * @return StandardConfig 839 | * @throws Exception\InvalidArgumentException 840 | */ 841 | public function setRememberMeSeconds($rememberMeSeconds) 842 | { 843 | if (! is_numeric($rememberMeSeconds)) { 844 | throw new Exception\InvalidArgumentException('Invalid remember_me_seconds; must be numeric'); 845 | } 846 | 847 | $rememberMeSeconds = (int) $rememberMeSeconds; 848 | if (1 > $rememberMeSeconds) { 849 | throw new Exception\InvalidArgumentException('Invalid remember_me_seconds; must be a positive integer'); 850 | } 851 | 852 | $this->rememberMeSeconds = $rememberMeSeconds; 853 | $this->setStorageOption('remember_me_seconds', $rememberMeSeconds); 854 | return $this; 855 | } 856 | 857 | /** 858 | * Get remember_me_seconds 859 | * 860 | * @return int 861 | */ 862 | public function getRememberMeSeconds() 863 | { 864 | if (null === $this->rememberMeSeconds) { 865 | $this->rememberMeSeconds = $this->getStorageOption('remember_me_seconds'); 866 | } 867 | return $this->rememberMeSeconds; 868 | } 869 | 870 | /** 871 | * Cast configuration to an array 872 | * 873 | * @return array 874 | */ 875 | public function toArray() 876 | { 877 | $extraOpts = [ 878 | 'cookie_domain' => $this->getCookieDomain(), 879 | 'cookie_httponly' => $this->getCookieHttpOnly(), 880 | 'cookie_lifetime' => $this->getCookieLifetime(), 881 | 'cookie_path' => $this->getCookiePath(), 882 | 'cookie_secure' => $this->getCookieSecure(), 883 | 'name' => $this->getName(), 884 | 'remember_me_seconds' => $this->getRememberMeSeconds(), 885 | 'save_path' => $this->getSavePath(), 886 | 'use_cookies' => $this->getUseCookies(), 887 | ]; 888 | return array_merge($this->options, $extraOpts); 889 | } 890 | 891 | /** 892 | * Intercept get*() and set*() methods 893 | * 894 | * Intercepts getters and setters and passes them to getOption() and setOption(), 895 | * respectively. 896 | * 897 | * @param string $method 898 | * @param array $args 899 | * @return mixed 900 | * @throws Exception\BadMethodCallException on non-getter/setter method 901 | */ 902 | public function __call($method, $args) 903 | { 904 | $prefix = substr($method, 0, 3); 905 | $option = substr($method, 3); 906 | $key = strtolower(preg_replace('#(?<=[a-z])([A-Z])#', '_\1', $option)); 907 | 908 | if ($prefix === 'set') { 909 | $value = array_shift($args); 910 | return $this->setOption($key, $value); 911 | } elseif ($prefix === 'get') { 912 | return $this->getOption($key); 913 | } else { 914 | throw new Exception\BadMethodCallException(sprintf( 915 | 'Method "%s" does not exist in %s', 916 | $method, 917 | get_class($this) 918 | )); 919 | } 920 | } 921 | } 922 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | $this->getDependencyConfig(), 21 | ]; 22 | } 23 | 24 | /** 25 | * Retrieve dependency config for zend-session. 26 | * 27 | * @return array 28 | */ 29 | public function getDependencyConfig() 30 | { 31 | return [ 32 | 'abstract_factories' => [ 33 | Service\ContainerAbstractServiceFactory::class, 34 | ], 35 | 'aliases' => [ 36 | SessionManager::class => ManagerInterface::class, 37 | ], 38 | 'factories' => [ 39 | Config\ConfigInterface::class => Service\SessionConfigFactory::class, 40 | ManagerInterface::class => Service\SessionManagerFactory::class, 41 | Storage\StorageInterface::class => Service\StorageFactory::class, 42 | ], 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Container.php: -------------------------------------------------------------------------------- 1 | offsetExists($key)) { 30 | return $ret; 31 | } 32 | $storage = $this->getStorage(); 33 | $name = $this->getName(); 34 | $ret =& $storage[$name][$key]; 35 | 36 | return $ret; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | $provider->getDependencyConfig(), 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/SaveHandler/Cache.php: -------------------------------------------------------------------------------- 1 | setCacheStorage($cacheStorage); 46 | } 47 | 48 | /** 49 | * Open Session 50 | * 51 | * @param string $savePath 52 | * @param string $name 53 | * @return bool 54 | */ 55 | public function open($savePath, $name) 56 | { 57 | // @todo figure out if we want to use these 58 | $this->sessionSavePath = $savePath; 59 | $this->sessionName = $name; 60 | 61 | return true; 62 | } 63 | 64 | /** 65 | * Close session 66 | * 67 | * @return bool 68 | */ 69 | public function close() 70 | { 71 | return true; 72 | } 73 | 74 | /** 75 | * Read session data 76 | * 77 | * @param string $id 78 | * @return string 79 | */ 80 | public function read($id) 81 | { 82 | return (string) $this->getCacheStorage()->getItem($id); 83 | } 84 | 85 | /** 86 | * Write session data 87 | * 88 | * @param string $id 89 | * @param string $data 90 | * @return bool 91 | */ 92 | public function write($id, $data) 93 | { 94 | return $this->getCacheStorage()->setItem($id, $data); 95 | } 96 | 97 | /** 98 | * Destroy session 99 | * 100 | * @param string $id 101 | * @return bool 102 | */ 103 | public function destroy($id) 104 | { 105 | $this->getCacheStorage()->getItem($id, $exists); 106 | if (! (bool) $exists) { 107 | return true; 108 | } 109 | 110 | return (bool) $this->getCacheStorage()->removeItem($id); 111 | } 112 | 113 | /** 114 | * Garbage Collection 115 | * 116 | * @param int $maxlifetime 117 | * @return bool 118 | */ 119 | public function gc($maxlifetime) 120 | { 121 | $cache = $this->getCacheStorage(); 122 | if ($cache instanceof ClearExpiredCacheStorage) { 123 | return $cache->clearExpired(); 124 | } 125 | return true; 126 | } 127 | 128 | /** 129 | * Set cache storage 130 | * 131 | * @param CacheStorage $cacheStorage 132 | * @return Cache 133 | */ 134 | public function setCacheStorage(CacheStorage $cacheStorage) 135 | { 136 | $this->cacheStorage = $cacheStorage; 137 | return $this; 138 | } 139 | 140 | /** 141 | * Get cache storage 142 | * 143 | * @return CacheStorage 144 | */ 145 | public function getCacheStorage() 146 | { 147 | return $this->cacheStorage; 148 | } 149 | 150 | /** 151 | * @deprecated Misspelled method - use getCacheStorage() instead 152 | */ 153 | public function getCacheStorge() 154 | { 155 | return $this->getCacheStorage(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/SaveHandler/DbTableGateway.php: -------------------------------------------------------------------------------- 1 | tableGateway = $tableGateway; 58 | $this->options = $options; 59 | } 60 | 61 | /** 62 | * Open Session 63 | * 64 | * @param string $savePath 65 | * @param string $name 66 | * @return bool 67 | */ 68 | public function open($savePath, $name) 69 | { 70 | $this->sessionSavePath = $savePath; 71 | $this->sessionName = $name; 72 | $this->lifetime = ini_get('session.gc_maxlifetime'); 73 | 74 | return true; 75 | } 76 | 77 | /** 78 | * Close session 79 | * 80 | * @return bool 81 | */ 82 | public function close() 83 | { 84 | return true; 85 | } 86 | 87 | /** 88 | * Read session data 89 | * 90 | * @param string $id 91 | * @param bool $destroyExpired Optional; true by default 92 | * @return string 93 | */ 94 | public function read($id, $destroyExpired = true) 95 | { 96 | $row = $this->tableGateway->select([ 97 | $this->options->getIdColumn() => $id, 98 | $this->options->getNameColumn() => $this->sessionName, 99 | ])->current(); 100 | 101 | if ($row) { 102 | if ($row->{$this->options->getModifiedColumn()} + 103 | $row->{$this->options->getLifetimeColumn()} > time()) { 104 | return (string) $row->{$this->options->getDataColumn()}; 105 | } 106 | if ($destroyExpired) { 107 | $this->destroy($id); 108 | } 109 | } 110 | return ''; 111 | } 112 | 113 | /** 114 | * Write session data 115 | * 116 | * @param string $id 117 | * @param string $data 118 | * @return bool 119 | */ 120 | public function write($id, $data) 121 | { 122 | $data = [ 123 | $this->options->getModifiedColumn() => time(), 124 | $this->options->getDataColumn() => (string) $data, 125 | ]; 126 | 127 | $rows = $this->tableGateway->select([ 128 | $this->options->getIdColumn() => $id, 129 | $this->options->getNameColumn() => $this->sessionName, 130 | ])->current(); 131 | 132 | if ($rows) { 133 | return (bool) $this->tableGateway->update($data, [ 134 | $this->options->getIdColumn() => $id, 135 | $this->options->getNameColumn() => $this->sessionName, 136 | ]); 137 | } 138 | $data[$this->options->getLifetimeColumn()] = $this->lifetime; 139 | $data[$this->options->getIdColumn()] = $id; 140 | $data[$this->options->getNameColumn()] = $this->sessionName; 141 | 142 | return (bool) $this->tableGateway->insert($data); 143 | } 144 | 145 | /** 146 | * Destroy session 147 | * 148 | * @param string $id 149 | * @return bool 150 | */ 151 | public function destroy($id) 152 | { 153 | if (! (bool) $this->read($id, false)) { 154 | return true; 155 | } 156 | 157 | return (bool) $this->tableGateway->delete([ 158 | $this->options->getIdColumn() => $id, 159 | $this->options->getNameColumn() => $this->sessionName, 160 | ]); 161 | } 162 | 163 | /** 164 | * Garbage Collection 165 | * 166 | * @param int $maxlifetime 167 | * @return true 168 | */ 169 | public function gc($maxlifetime) 170 | { 171 | $platform = $this->tableGateway->getAdapter()->getPlatform(); 172 | return (bool) $this->tableGateway->delete( 173 | sprintf( 174 | '%s < %d', 175 | $platform->quoteIdentifier($this->options->getModifiedColumn()), 176 | (time() - $this->lifetime) 177 | ) 178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/SaveHandler/DbTableGatewayOptions.php: -------------------------------------------------------------------------------- 1 | idColumn = $idColumn; 62 | return $this; 63 | } 64 | 65 | /** 66 | * Get Id Column 67 | * 68 | * @return string 69 | */ 70 | public function getIdColumn() 71 | { 72 | return $this->idColumn; 73 | } 74 | 75 | /** 76 | * Set Name Column 77 | * 78 | * @param string $nameColumn 79 | * @return DbTableGatewayOptions 80 | * @throws Exception\InvalidArgumentException 81 | */ 82 | public function setNameColumn($nameColumn) 83 | { 84 | $nameColumn = (string) $nameColumn; 85 | if (strlen($nameColumn) === 0) { 86 | throw new Exception\InvalidArgumentException('$nameColumn must be a non-empty string'); 87 | } 88 | $this->nameColumn = $nameColumn; 89 | return $this; 90 | } 91 | 92 | /** 93 | * Get Name Column 94 | * 95 | * @return string 96 | */ 97 | public function getNameColumn() 98 | { 99 | return $this->nameColumn; 100 | } 101 | 102 | /** 103 | * Set Data Column 104 | * 105 | * @param string $dataColumn 106 | * @return DbTableGatewayOptions 107 | * @throws Exception\InvalidArgumentException 108 | */ 109 | public function setDataColumn($dataColumn) 110 | { 111 | $dataColumn = (string) $dataColumn; 112 | if (strlen($dataColumn) === 0) { 113 | throw new Exception\InvalidArgumentException('$dataColumn must be a non-empty string'); 114 | } 115 | $this->dataColumn = $dataColumn; 116 | return $this; 117 | } 118 | 119 | /** 120 | * Get Data Column 121 | * 122 | * @return string 123 | */ 124 | public function getDataColumn() 125 | { 126 | return $this->dataColumn; 127 | } 128 | 129 | /** 130 | * Set Lifetime Column 131 | * 132 | * @param string $lifetimeColumn 133 | * @return DbTableGatewayOptions 134 | * @throws Exception\InvalidArgumentException 135 | */ 136 | public function setLifetimeColumn($lifetimeColumn) 137 | { 138 | $lifetimeColumn = (string) $lifetimeColumn; 139 | if (strlen($lifetimeColumn) === 0) { 140 | throw new Exception\InvalidArgumentException('$lifetimeColumn must be a non-empty string'); 141 | } 142 | $this->lifetimeColumn = $lifetimeColumn; 143 | return $this; 144 | } 145 | 146 | /** 147 | * Get Lifetime Column 148 | * 149 | * @return string 150 | */ 151 | public function getLifetimeColumn() 152 | { 153 | return $this->lifetimeColumn; 154 | } 155 | 156 | /** 157 | * Set Modified Column 158 | * 159 | * @param string $modifiedColumn 160 | * @return DbTableGatewayOptions 161 | * @throws Exception\InvalidArgumentException 162 | */ 163 | public function setModifiedColumn($modifiedColumn) 164 | { 165 | $modifiedColumn = (string) $modifiedColumn; 166 | if (strlen($modifiedColumn) === 0) { 167 | throw new Exception\InvalidArgumentException('$modifiedColumn must be a non-empty string'); 168 | } 169 | $this->modifiedColumn = $modifiedColumn; 170 | return $this; 171 | } 172 | 173 | /** 174 | * Get Modified Column 175 | * 176 | * @return string 177 | */ 178 | public function getModifiedColumn() 179 | { 180 | return $this->modifiedColumn; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/SaveHandler/MongoDB.php: -------------------------------------------------------------------------------- 1 | getDatabase())) { 65 | throw new InvalidArgumentException('The database option cannot be empty'); 66 | } 67 | 68 | if (null === ($collection = $options->getCollection())) { 69 | throw new InvalidArgumentException('The collection option cannot be empty'); 70 | } 71 | 72 | $this->mongoClient = $mongoClient; 73 | $this->options = $options; 74 | } 75 | 76 | /** 77 | * Open session 78 | * 79 | * @param string $savePath 80 | * @param string $name 81 | * @return bool 82 | */ 83 | public function open($savePath, $name) 84 | { 85 | // Note: session save path is not used 86 | $this->sessionName = $name; 87 | $this->lifetime = (int) ini_get('session.gc_maxlifetime'); 88 | 89 | $this->mongoCollection = $this->mongoClient->selectCollection( 90 | $this->options->getDatabase(), 91 | $this->options->getCollection() 92 | ); 93 | 94 | $this->mongoCollection->createIndex( 95 | [$this->options->getModifiedField() => 1], 96 | $this->options->useExpireAfterSecondsIndex() ? ['expireAfterSeconds' => $this->lifetime] : [] 97 | ); 98 | 99 | return true; 100 | } 101 | 102 | /** 103 | * Close session 104 | * 105 | * @return bool 106 | */ 107 | public function close() 108 | { 109 | return true; 110 | } 111 | 112 | /** 113 | * Read session data 114 | * 115 | * @param string $id 116 | * @return string 117 | */ 118 | public function read($id) 119 | { 120 | $session = $this->mongoCollection->findOne([ 121 | '_id' => $id, 122 | $this->options->getNameField() => $this->sessionName, 123 | ]); 124 | 125 | if (null !== $session) { 126 | // check if session has expired if index is not used 127 | if (! $this->options->useExpireAfterSecondsIndex()) { 128 | $timestamp = $session[$this->options->getLifetimeField()]; 129 | $timestamp += floor(((string)$session[$this->options->getModifiedField()]) / 1000); 130 | 131 | // session expired 132 | if ($timestamp <= time()) { 133 | $this->destroy($id); 134 | return ''; 135 | } 136 | } 137 | return $session[$this->options->getDataField()]->getData(); 138 | } 139 | 140 | return ''; 141 | } 142 | 143 | /** 144 | * Write session data 145 | * 146 | * @param string $id 147 | * @param string $data 148 | * @return bool 149 | */ 150 | public function write($id, $data) 151 | { 152 | $saveOptions = array_replace( 153 | $this->options->getSaveOptions(), 154 | ['upsert' => true, 'multiple' => false] 155 | ); 156 | 157 | $criteria = [ 158 | '_id' => $id, 159 | $this->options->getNameField() => $this->sessionName, 160 | ]; 161 | 162 | $newObj = [ 163 | '$set' => [ 164 | $this->options->getDataField() => new Binary((string)$data, Binary::TYPE_GENERIC), 165 | $this->options->getLifetimeField() => $this->lifetime, 166 | $this->options->getModifiedField() => new UTCDatetime(floor(microtime(true) * 1000)), 167 | ], 168 | ]; 169 | 170 | /* Note: a MongoCursorException will be thrown if a record with this ID 171 | * already exists with a different session name, since the upsert query 172 | * cannot insert a new document with the same ID and new session name. 173 | * This should only happen if ID's are not unique or if the session name 174 | * is altered mid-process. 175 | */ 176 | $result = $this->mongoCollection->updateOne($criteria, $newObj, $saveOptions); 177 | 178 | return $result->isAcknowledged(); 179 | } 180 | 181 | /** 182 | * Destroy session 183 | * 184 | * @param string $id 185 | * @return bool 186 | */ 187 | public function destroy($id) 188 | { 189 | $result = $this->mongoCollection->deleteOne( 190 | [ 191 | '_id' => $id, 192 | $this->options->getNameField() => $this->sessionName, 193 | ], 194 | $this->options->getSaveOptions() 195 | ); 196 | 197 | return $result->isAcknowledged(); 198 | } 199 | 200 | /** 201 | * Garbage collection 202 | * 203 | * Note: MongoDB 2.2+ supports TTL collections, which may be used in place 204 | * of this method by indexing the "modified" field with an 205 | * "expireAfterSeconds" option. Regardless of whether TTL collections are 206 | * used, consider indexing this field to make the remove query more 207 | * efficient. 208 | * 209 | * @see http://docs.mongodb.org/manual/tutorial/expire-data/ 210 | * @param int $maxlifetime 211 | * @return bool 212 | */ 213 | public function gc($maxlifetime) 214 | { 215 | /* Note: unlike DbTableGateway, we do not use the lifetime field in 216 | * each document. Doing so would require a $where query to work with the 217 | * computed value (modified + lifetime) and be very inefficient. 218 | */ 219 | $microseconds = floor(microtime(true) * 1000) - $maxlifetime * 1000; 220 | 221 | $result = $this->mongoCollection->deleteMany( 222 | [ 223 | $this->options->getModifiedField() => ['$lt' => new UTCDateTime($microseconds)], 224 | ], 225 | $this->options->getSaveOptions() 226 | ); 227 | 228 | return $result->isAcknowledged(); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/SaveHandler/MongoDBOptions.php: -------------------------------------------------------------------------------- 1 | 1]; 39 | 40 | /** 41 | * Name field 42 | * 43 | * @var string 44 | */ 45 | protected $nameField = 'name'; 46 | 47 | /** 48 | * Data field 49 | * 50 | * @var string 51 | */ 52 | protected $dataField = 'data'; 53 | 54 | /** 55 | * Lifetime field 56 | * 57 | * @var string 58 | */ 59 | protected $lifetimeField = 'lifetime'; 60 | 61 | /** 62 | * Modified field 63 | * 64 | * @var string 65 | */ 66 | protected $modifiedField = 'modified'; 67 | 68 | /** 69 | * Use expireAfterSeconds index 70 | * 71 | * @var bool 72 | */ 73 | protected $useExpireAfterSecondsIndex = false; 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function __construct($options = null) 79 | { 80 | parent::__construct($options); 81 | 82 | $mongoVersion = phpversion('mongo') ?: '0.0.0'; 83 | if ($this->saveOptions === ['w' => 1] && version_compare($mongoVersion, '1.3.0', '<')) { 84 | $this->saveOptions = ['safe' => true]; 85 | } 86 | } 87 | 88 | /** 89 | * Override AbstractOptions::__set 90 | * 91 | * Validates value if save options are being set. 92 | * 93 | * @param string $key 94 | * @param mixed $value 95 | */ 96 | public function __set($key, $value) 97 | { 98 | if (strtolower($key) !== 'saveoptions') { 99 | return parent::__set($key, $value); 100 | } 101 | 102 | if (! is_array($value)) { 103 | throw new InvalidArgumentException('Expected array for save options'); 104 | } 105 | $this->setSaveOptions($value); 106 | } 107 | 108 | /** 109 | * Set database name 110 | * 111 | * @param string $database 112 | * @return MongoDBOptions 113 | * @throws InvalidArgumentException 114 | */ 115 | public function setDatabase($database) 116 | { 117 | $database = (string) $database; 118 | if (strlen($database) === 0) { 119 | throw new InvalidArgumentException('$database must be a non-empty string'); 120 | } 121 | $this->database = $database; 122 | return $this; 123 | } 124 | 125 | /** 126 | * Get database name 127 | * 128 | * @return string 129 | */ 130 | public function getDatabase() 131 | { 132 | return $this->database; 133 | } 134 | 135 | /** 136 | * Set collection name 137 | * 138 | * @param string $collection 139 | * @return MongoDBOptions 140 | * @throws InvalidArgumentException 141 | */ 142 | public function setCollection($collection) 143 | { 144 | $collection = (string) $collection; 145 | if (strlen($collection) === 0) { 146 | throw new InvalidArgumentException('$collection must be a non-empty string'); 147 | } 148 | $this->collection = $collection; 149 | return $this; 150 | } 151 | 152 | /** 153 | * Get collection name 154 | * 155 | * @return string 156 | */ 157 | public function getCollection() 158 | { 159 | return $this->collection; 160 | } 161 | 162 | /** 163 | * Set save options 164 | * 165 | * @see http://php.net/manual/en/mongocollection.save.php 166 | * @param array $saveOptions 167 | * @return MongoDBOptions 168 | */ 169 | public function setSaveOptions(array $saveOptions) 170 | { 171 | $this->saveOptions = $saveOptions; 172 | return $this; 173 | } 174 | 175 | /** 176 | * Get save options 177 | * 178 | * @return string 179 | */ 180 | public function getSaveOptions() 181 | { 182 | return $this->saveOptions; 183 | } 184 | 185 | /** 186 | * Set name field 187 | * 188 | * @param string $nameField 189 | * @return MongoDBOptions 190 | * @throws InvalidArgumentException 191 | */ 192 | public function setNameField($nameField) 193 | { 194 | $nameField = (string) $nameField; 195 | if (strlen($nameField) === 0) { 196 | throw new InvalidArgumentException('$nameField must be a non-empty string'); 197 | } 198 | $this->nameField = $nameField; 199 | return $this; 200 | } 201 | 202 | /** 203 | * Get name field 204 | * 205 | * @return string 206 | */ 207 | public function getNameField() 208 | { 209 | return $this->nameField; 210 | } 211 | 212 | /** 213 | * Set data field 214 | * 215 | * @param string $dataField 216 | * @return MongoDBOptions 217 | * @throws InvalidArgumentException 218 | */ 219 | public function setDataField($dataField) 220 | { 221 | $dataField = (string) $dataField; 222 | if (strlen($dataField) === 0) { 223 | throw new InvalidArgumentException('$dataField must be a non-empty string'); 224 | } 225 | $this->dataField = $dataField; 226 | return $this; 227 | } 228 | 229 | /** 230 | * Get data field 231 | * 232 | * @return string 233 | */ 234 | public function getDataField() 235 | { 236 | return $this->dataField; 237 | } 238 | 239 | /** 240 | * Set lifetime field 241 | * 242 | * @param string $lifetimeField 243 | * @return MongoDBOptions 244 | * @throws InvalidArgumentException 245 | */ 246 | public function setLifetimeField($lifetimeField) 247 | { 248 | $lifetimeField = (string) $lifetimeField; 249 | if (strlen($lifetimeField) === 0) { 250 | throw new InvalidArgumentException('$lifetimeField must be a non-empty string'); 251 | } 252 | $this->lifetimeField = $lifetimeField; 253 | return $this; 254 | } 255 | 256 | /** 257 | * Get lifetime Field 258 | * 259 | * @return string 260 | */ 261 | public function getLifetimeField() 262 | { 263 | return $this->lifetimeField; 264 | } 265 | 266 | /** 267 | * Set Modified Field 268 | * 269 | * @param string $modifiedField 270 | * @return MongoDBOptions 271 | * @throws InvalidArgumentException 272 | */ 273 | public function setModifiedField($modifiedField) 274 | { 275 | $modifiedField = (string) $modifiedField; 276 | if (strlen($modifiedField) === 0) { 277 | throw new InvalidArgumentException('$modifiedField must be a non-empty string'); 278 | } 279 | $this->modifiedField = $modifiedField; 280 | return $this; 281 | } 282 | 283 | /** 284 | * Get modified Field 285 | * 286 | * @return string 287 | */ 288 | public function getModifiedField() 289 | { 290 | return $this->modifiedField; 291 | } 292 | 293 | /** 294 | * @return boolean 295 | */ 296 | public function useExpireAfterSecondsIndex() 297 | { 298 | return $this->useExpireAfterSecondsIndex; 299 | } 300 | 301 | /** 302 | * Enable expireAfterSeconds index. 303 | * 304 | * @see http://docs.mongodb.org/manual/tutorial/expire-data/ 305 | * @param boolean $useExpireAfterSecondsIndex 306 | */ 307 | public function setUseExpireAfterSecondsIndex($useExpireAfterSecondsIndex) 308 | { 309 | $this->useExpireAfterSecondsIndex = (bool) $useExpireAfterSecondsIndex; 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/SaveHandler/SaveHandlerInterface.php: -------------------------------------------------------------------------------- 1 | 24 | * return array( 25 | * 'session_containers' => array( 26 | * 'SessionContainer\sample', 27 | * 'my_sample_session_container', 28 | * 'MySessionContainer', 29 | * ), 30 | * ); 31 | * 32 | * 33 | * 34 | * $container = $services->get('MySessionContainer'); 35 | * 36 | */ 37 | class ContainerAbstractServiceFactory implements AbstractFactoryInterface 38 | { 39 | /** 40 | * Cached container configuration 41 | * 42 | * @var array 43 | */ 44 | protected $config; 45 | 46 | /** 47 | * Configuration key in which session containers live 48 | * 49 | * @var string 50 | */ 51 | protected $configKey = 'session_containers'; 52 | 53 | /** 54 | * @var ManagerInterface 55 | */ 56 | protected $sessionManager; 57 | 58 | /** 59 | * Can we create an instance of the given service? (v3 usage). 60 | * 61 | * @param ContainerInterface $container 62 | * @param string $requestedName 63 | * @return bool 64 | */ 65 | public function canCreate(ContainerInterface $container, $requestedName) 66 | { 67 | $config = $this->getConfig($container); 68 | if (empty($config)) { 69 | return false; 70 | } 71 | 72 | $containerName = $this->normalizeContainerName($requestedName); 73 | return array_key_exists($containerName, $config); 74 | } 75 | 76 | /** 77 | * Can we create an instance of the given service? (v2 usage) 78 | * 79 | * @param ServiceLocatorInterface $container 80 | * @param string $name 81 | * @param string $requestedName 82 | * @return bool 83 | */ 84 | public function canCreateServiceWithName(ServiceLocatorInterface $container, $name, $requestedName) 85 | { 86 | return $this->canCreate($container, $requestedName); 87 | } 88 | 89 | /** 90 | * Create and return a named container (v3 usage). 91 | * 92 | * @param ContainerInterface $container 93 | * @param string $requestedName 94 | * @return Container 95 | */ 96 | public function __invoke(ContainerInterface $container, $requestedName, array $options = null) 97 | { 98 | $manager = $this->getSessionManager($container); 99 | return new Container($requestedName, $manager); 100 | } 101 | 102 | /** 103 | * Create and return a named container (v2 usage). 104 | * 105 | * @param ContainerInterface $container 106 | * @param string $requestedName 107 | * @return Container 108 | */ 109 | public function createServiceWithName(ServiceLocatorInterface $container, $name, $requestedName) 110 | { 111 | return $this($container, $requestedName); 112 | } 113 | 114 | /** 115 | * Retrieve config from service locator, and cache for later 116 | * 117 | * @param ContainerInterface $container 118 | * @return false|array 119 | */ 120 | protected function getConfig(ContainerInterface $container) 121 | { 122 | if (null !== $this->config) { 123 | return $this->config; 124 | } 125 | 126 | if (! $container->has('config')) { 127 | $this->config = []; 128 | return $this->config; 129 | } 130 | 131 | $config = $container->get('config'); 132 | if (! isset($config[$this->configKey]) || ! is_array($config[$this->configKey])) { 133 | $this->config = []; 134 | return $this->config; 135 | } 136 | 137 | $config = $config[$this->configKey]; 138 | $config = array_flip($config); 139 | 140 | $this->config = array_change_key_case($config); 141 | 142 | return $this->config; 143 | } 144 | 145 | /** 146 | * Retrieve the session manager instance, if any 147 | * 148 | * @param ContainerInterface $container 149 | * @return null|ManagerInterface 150 | */ 151 | protected function getSessionManager(ContainerInterface $container) 152 | { 153 | if ($this->sessionManager !== null) { 154 | return $this->sessionManager; 155 | } 156 | 157 | if ($container->has(ManagerInterface::class)) { 158 | $this->sessionManager = $container->get(ManagerInterface::class); 159 | } 160 | 161 | return $this->sessionManager; 162 | } 163 | 164 | /** 165 | * Normalize the container name in order to perform a lookup 166 | * 167 | * @param string $name 168 | * @return string 169 | */ 170 | protected function normalizeContainerName($name) 171 | { 172 | return strtolower($name); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Service/SessionConfigFactory.php: -------------------------------------------------------------------------------- 1 | get('config'); 37 | if (! isset($config['session_config']) || ! is_array($config['session_config'])) { 38 | throw new ServiceNotCreatedException( 39 | 'Configuration is missing a "session_config" key, or the value of that key is not an array' 40 | ); 41 | } 42 | 43 | $class = SessionConfig::class; 44 | $config = $config['session_config']; 45 | if (isset($config['config_class'])) { 46 | if (! class_exists($config['config_class'])) { 47 | throw new ServiceNotCreatedException(sprintf( 48 | 'Invalid configuration class "%s" specified in "config_class" session configuration; ' 49 | . 'must be a valid class', 50 | $config['config_class'] 51 | )); 52 | } 53 | $class = $config['config_class']; 54 | unset($config['config_class']); 55 | } 56 | 57 | $sessionConfig = new $class(); 58 | if (! $sessionConfig instanceof ConfigInterface) { 59 | throw new ServiceNotCreatedException(sprintf( 60 | 'Invalid configuration class "%s" specified in "config_class" session configuration; must implement %s', 61 | $class, 62 | ConfigInterface::class 63 | )); 64 | } 65 | $sessionConfig->setOptions($config); 66 | 67 | return $sessionConfig; 68 | } 69 | 70 | /** 71 | * Create and return a config instance (v2 usage). 72 | * 73 | * @param ServiceLocatorInterface $services 74 | * @param null|string $canonicalName 75 | * @param string $requestedName 76 | * @return ConfigInterface 77 | */ 78 | public function createService( 79 | ServiceLocatorInterface $services, 80 | $canonicalName = null, 81 | $requestedName = ConfigInterface::class 82 | ) { 83 | return $this($services, $requestedName); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Service/SessionManagerFactory.php: -------------------------------------------------------------------------------- 1 | true, 30 | ]; 31 | 32 | /** 33 | * Create session manager object (v3 usage). 34 | * 35 | * Will consume any combination (or zero) of the following services, when 36 | * present, to construct the SessionManager instance: 37 | * 38 | * - Zend\Session\Config\ConfigInterface 39 | * - Zend\Session\Storage\StorageInterface 40 | * - Zend\Session\SaveHandler\SaveHandlerInterface 41 | * 42 | * The first two have corresponding factories inside this namespace. The 43 | * last, however, does not, due to the differences in implementations, and 44 | * the fact that save handlers will often be written in userland. As such 45 | * if you wish to attach a save handler to the manager, you will need to 46 | * write your own factory, and assign it to the service name 47 | * "Zend\Session\SaveHandler\SaveHandlerInterface", (or alias that name 48 | * to your own service). 49 | * 50 | * You can configure limited behaviors via the "session_manager" key of the 51 | * Config service. Currently, these include: 52 | * 53 | * - enable_default_container_manager: whether to inject the created instance 54 | * as the default manager for Container instances. The default value for 55 | * this is true; set it to false to disable. 56 | * - validators: ... 57 | * 58 | * @param ContainerInterface $container 59 | * @param string $requestedName 60 | * @param array $options 61 | * @return SessionManager 62 | */ 63 | public function __invoke(ContainerInterface $container, $requestedName, array $options = null) 64 | { 65 | $config = null; 66 | $storage = null; 67 | $saveHandler = null; 68 | $validators = []; 69 | $managerConfig = $this->defaultManagerConfig; 70 | $options = []; 71 | 72 | if ($container->has(ConfigInterface::class)) { 73 | $config = $container->get(ConfigInterface::class); 74 | if (! $config instanceof ConfigInterface) { 75 | throw new ServiceNotCreatedException(sprintf( 76 | 'SessionManager requires that the %s service implement %s; received "%s"', 77 | ConfigInterface::class, 78 | ConfigInterface::class, 79 | (is_object($config) ? get_class($config) : gettype($config)) 80 | )); 81 | } 82 | } 83 | 84 | if ($container->has(StorageInterface::class)) { 85 | $storage = $container->get(StorageInterface::class); 86 | if (! $storage instanceof StorageInterface) { 87 | throw new ServiceNotCreatedException(sprintf( 88 | 'SessionManager requires that the %s service implement %s; received "%s"', 89 | StorageInterface::class, 90 | StorageInterface::class, 91 | (is_object($storage) ? get_class($storage) : gettype($storage)) 92 | )); 93 | } 94 | } 95 | 96 | if ($container->has(SaveHandlerInterface::class)) { 97 | $saveHandler = $container->get(SaveHandlerInterface::class); 98 | if (! $saveHandler instanceof SaveHandlerInterface) { 99 | throw new ServiceNotCreatedException(sprintf( 100 | 'SessionManager requires that the %s service implement %s; received "%s"', 101 | SaveHandlerInterface::class, 102 | SaveHandlerInterface::class, 103 | (is_object($saveHandler) ? get_class($saveHandler) : gettype($saveHandler)) 104 | )); 105 | } 106 | } 107 | 108 | // Get session manager configuration, if any, and merge with default configuration 109 | if ($container->has('config')) { 110 | $configService = $container->get('config'); 111 | if (isset($configService['session_manager']) 112 | && is_array($configService['session_manager']) 113 | ) { 114 | $managerConfig = array_merge($managerConfig, $configService['session_manager']); 115 | } 116 | 117 | if (isset($managerConfig['validators'])) { 118 | $validators = $managerConfig['validators']; 119 | } 120 | 121 | if (isset($managerConfig['options'])) { 122 | $options = $managerConfig['options']; 123 | } 124 | } 125 | 126 | $managerClass = class_exists($requestedName) ? $requestedName : SessionManager::class; 127 | if (! is_subclass_of($managerClass, ManagerInterface::class)) { 128 | throw new ServiceNotCreatedException(sprintf( 129 | 'SessionManager requires that the %s service implement %s', 130 | $managerClass, 131 | ManagerInterface::class 132 | )); 133 | } 134 | 135 | $manager = new $managerClass($config, $storage, $saveHandler, $validators, $options); 136 | 137 | // If configuration enables the session manager as the default manager for container 138 | // instances, do so. 139 | if (isset($managerConfig['enable_default_container_manager']) 140 | && $managerConfig['enable_default_container_manager'] 141 | ) { 142 | Container::setDefaultManager($manager); 143 | } 144 | 145 | return $manager; 146 | } 147 | 148 | /** 149 | * Create a SessionManager instance (v2 usage) 150 | * 151 | * @param ServiceLocatorInterface $services 152 | * @param null|string $canonicalName 153 | * @param string $requestedName 154 | * @return SessionManager 155 | */ 156 | public function createService( 157 | ServiceLocatorInterface $services, 158 | $canonicalName = null, 159 | $requestedName = SessionManager::class 160 | ) { 161 | return $this($services, $requestedName); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Service/StorageFactory.php: -------------------------------------------------------------------------------- 1 | get('config'); 36 | if (! isset($config['session_storage']) || ! is_array($config['session_storage'])) { 37 | throw new ServiceNotCreatedException( 38 | 'Configuration is missing a "session_storage" key, or the value of that key is not an array' 39 | ); 40 | } 41 | 42 | $config = $config['session_storage']; 43 | if (! isset($config['type'])) { 44 | throw new ServiceNotCreatedException( 45 | '"session_storage" configuration is missing a "type" key' 46 | ); 47 | } 48 | $type = $config['type']; 49 | $options = isset($config['options']) ? $config['options'] : []; 50 | 51 | try { 52 | $storage = Factory::factory($type, $options); 53 | } catch (SessionException $e) { 54 | throw new ServiceNotCreatedException(sprintf( 55 | 'Factory is unable to create StorageInterface instance: %s', 56 | $e->getMessage() 57 | ), $e->getCode(), $e); 58 | } 59 | 60 | return $storage; 61 | } 62 | 63 | /** 64 | * Create and return a storage instance (v2 usage). 65 | * 66 | * @param ServiceLocatorInterface $services 67 | * @param null|string $canonicalName 68 | * @param string $requestedName 69 | * @return StorageInterface 70 | */ 71 | public function createService( 72 | ServiceLocatorInterface $services, 73 | $canonicalName = null, 74 | $requestedName = StorageInterface::class 75 | ) { 76 | return $this($services, $requestedName); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/SessionManager.php: -------------------------------------------------------------------------------- 1 | true, 28 | 'clear_storage' => false, 29 | ]; 30 | 31 | /** 32 | * @var array Default session manager options 33 | */ 34 | protected $defaultOptions = [ 35 | 'attach_default_validators' => true, 36 | ]; 37 | 38 | /** 39 | * @var array Default validators 40 | */ 41 | protected $defaultValidators = [ 42 | Validator\Id::class, 43 | ]; 44 | 45 | /** 46 | * @var string value returned by session_name() 47 | */ 48 | protected $name; 49 | 50 | /** 51 | * @var EventManagerInterface Validation chain to determine if session is valid 52 | */ 53 | protected $validatorChain; 54 | 55 | /** 56 | * Constructor 57 | * 58 | * @param Config\ConfigInterface|null $config 59 | * @param Storage\StorageInterface|null $storage 60 | * @param SaveHandler\SaveHandlerInterface|null $saveHandler 61 | * @param array $validators 62 | * @param array $options 63 | * @throws Exception\RuntimeException 64 | */ 65 | public function __construct( 66 | Config\ConfigInterface $config = null, 67 | Storage\StorageInterface $storage = null, 68 | SaveHandler\SaveHandlerInterface $saveHandler = null, 69 | array $validators = [], 70 | array $options = [] 71 | ) { 72 | $options = array_merge($this->defaultOptions, $options); 73 | if ($options['attach_default_validators']) { 74 | $validators = array_merge($this->defaultValidators, $validators); 75 | } 76 | 77 | parent::__construct($config, $storage, $saveHandler, $validators); 78 | register_shutdown_function([$this, 'writeClose']); 79 | } 80 | 81 | /** 82 | * Does a session exist and is it currently active? 83 | * 84 | * @return bool 85 | */ 86 | public function sessionExists() 87 | { 88 | if (session_status() == PHP_SESSION_ACTIVE) { 89 | return true; 90 | } 91 | $sid = defined('SID') ? constant('SID') : false; 92 | if ($sid !== false && $this->getId()) { 93 | return true; 94 | } 95 | if (headers_sent()) { 96 | return true; 97 | } 98 | return false; 99 | } 100 | 101 | /** 102 | * Start session 103 | * 104 | * if No session currently exists, attempt to start it. Calls 105 | * {@link isValid()} once session_start() is called, and raises an 106 | * exception if validation fails. 107 | * 108 | * @param bool $preserveStorage If set to true, current session storage will not be overwritten by the 109 | * contents of $_SESSION. 110 | * @return void 111 | * @throws Exception\RuntimeException 112 | */ 113 | public function start($preserveStorage = false) 114 | { 115 | if ($this->sessionExists()) { 116 | return; 117 | } 118 | 119 | $saveHandler = $this->getSaveHandler(); 120 | if ($saveHandler instanceof SaveHandler\SaveHandlerInterface) { 121 | // register the session handler with ext/session 122 | $this->registerSaveHandler($saveHandler); 123 | } 124 | 125 | $oldSessionData = []; 126 | if (isset($_SESSION)) { 127 | $oldSessionData = $_SESSION; 128 | 129 | // convert session data to plain array that’ll be acceptable as 130 | // ArrayUtils::merge parameter 131 | if ($oldSessionData instanceof Storage\StorageInterface) { 132 | $oldSessionData = $oldSessionData->toArray(); 133 | } elseif ($oldSessionData instanceof Traversable) { 134 | $oldSessionData = iterator_to_array($oldSessionData); 135 | } 136 | } 137 | 138 | session_start(); 139 | 140 | if (! empty($oldSessionData) && is_array($oldSessionData)) { 141 | $_SESSION = ArrayUtils::merge($oldSessionData, $_SESSION, true); 142 | } 143 | 144 | $storage = $this->getStorage(); 145 | 146 | // Since session is starting, we need to potentially repopulate our 147 | // session storage 148 | if ($storage instanceof Storage\SessionStorage && $_SESSION !== $storage) { 149 | if (! $preserveStorage) { 150 | $storage->fromArray($_SESSION); 151 | } 152 | $_SESSION = $storage; 153 | } elseif ($storage instanceof Storage\StorageInitializationInterface) { 154 | $storage->init($_SESSION); 155 | } 156 | 157 | $this->initializeValidatorChain(); 158 | 159 | if (! $this->isValid()) { 160 | throw new Exception\RuntimeException('Session validation failed'); 161 | } 162 | } 163 | 164 | /** 165 | * Create validators, insert reference value and add them to the validator chain 166 | */ 167 | protected function initializeValidatorChain() 168 | { 169 | $validatorChain = $this->getValidatorChain(); 170 | $validatorValues = $this->getStorage()->getMetadata('_VALID'); 171 | 172 | foreach ($this->validators as $validator) { 173 | // Ignore validators which are already present in Storage 174 | if (is_array($validatorValues) && array_key_exists($validator, $validatorValues)) { 175 | continue; 176 | } 177 | 178 | $validator = new $validator(null); 179 | $validatorChain->attach('session.validate', [$validator, 'isValid']); 180 | } 181 | } 182 | 183 | /** 184 | * Destroy/end a session 185 | * 186 | * @param array $options See {@link $defaultDestroyOptions} 187 | * @return void 188 | */ 189 | public function destroy(array $options = null) 190 | { 191 | if (! $this->sessionExists()) { 192 | return; 193 | } 194 | 195 | if (null === $options) { 196 | $options = $this->defaultDestroyOptions; 197 | } else { 198 | $options = array_merge($this->defaultDestroyOptions, $options); 199 | } 200 | 201 | session_destroy(); 202 | if ($options['send_expire_cookie']) { 203 | $this->expireSessionCookie(); 204 | } 205 | 206 | if ($options['clear_storage']) { 207 | $this->getStorage()->clear(); 208 | } 209 | } 210 | 211 | /** 212 | * Write session to save handler and close 213 | * 214 | * Once done, the Storage object will be marked as isImmutable. 215 | * 216 | * @return void 217 | */ 218 | public function writeClose() 219 | { 220 | // The assumption is that we're using PHP's ext/session. 221 | // session_write_close() will actually overwrite $_SESSION with an 222 | // empty array on completion -- which leads to a mismatch between what 223 | // is in the storage object and $_SESSION. To get around this, we 224 | // temporarily reset $_SESSION to an array, and then re-link it to 225 | // the storage object. 226 | // 227 | // Additionally, while you _can_ write to $_SESSION following a 228 | // session_write_close() operation, no changes made to it will be 229 | // flushed to the session handler. As such, we now mark the storage 230 | // object isImmutable. 231 | $storage = $this->getStorage(); 232 | if (! $storage->isImmutable()) { 233 | $_SESSION = $storage->toArray(true); 234 | session_write_close(); 235 | $storage->fromArray($_SESSION); 236 | $storage->markImmutable(); 237 | } 238 | } 239 | 240 | /** 241 | * Attempt to set the session name 242 | * 243 | * If the session has already been started, or if the name provided fails 244 | * validation, an exception will be raised. 245 | * 246 | * @param string $name 247 | * @return SessionManager 248 | * @throws Exception\InvalidArgumentException 249 | */ 250 | public function setName($name) 251 | { 252 | if ($this->sessionExists()) { 253 | throw new Exception\InvalidArgumentException( 254 | 'Cannot set session name after a session has already started' 255 | ); 256 | } 257 | 258 | if (! preg_match('/^[a-zA-Z0-9]+$/', $name)) { 259 | throw new Exception\InvalidArgumentException( 260 | 'Name provided contains invalid characters; must be alphanumeric only' 261 | ); 262 | } 263 | 264 | $this->name = $name; 265 | session_name($name); 266 | return $this; 267 | } 268 | 269 | /** 270 | * Get session name 271 | * 272 | * Proxies to {@link session_name()}. 273 | * 274 | * @return string 275 | */ 276 | public function getName() 277 | { 278 | if (null === $this->name) { 279 | // If we're grabbing via session_name(), we don't need our 280 | // validation routine; additionally, calling setName() after 281 | // session_start() can lead to issues, and often we just need the name 282 | // in order to do things such as setting cookies. 283 | $this->name = session_name(); 284 | } 285 | return $this->name; 286 | } 287 | 288 | /** 289 | * Set session ID 290 | * 291 | * Can safely be called in the middle of a session. 292 | * 293 | * @param string $id 294 | * @return SessionManager 295 | */ 296 | public function setId($id) 297 | { 298 | if ($this->sessionExists()) { 299 | throw new Exception\RuntimeException( 300 | 'Session has already been started, to change the session ID call regenerateId()' 301 | ); 302 | } 303 | session_id($id); 304 | return $this; 305 | } 306 | 307 | /** 308 | * Get session ID 309 | * 310 | * Proxies to {@link session_id()} 311 | * 312 | * @return string 313 | */ 314 | public function getId() 315 | { 316 | return session_id(); 317 | } 318 | 319 | /** 320 | * Regenerate id 321 | * 322 | * Regenerate the session ID, using session save handler's 323 | * native ID generation Can safely be called in the middle of a session. 324 | * 325 | * @param bool $deleteOldSession 326 | * @return SessionManager 327 | */ 328 | public function regenerateId($deleteOldSession = true) 329 | { 330 | if ($this->sessionExists()) { 331 | session_regenerate_id((bool) $deleteOldSession); 332 | } 333 | 334 | return $this; 335 | } 336 | 337 | /** 338 | * Set the TTL (in seconds) for the session cookie expiry 339 | * 340 | * Can safely be called in the middle of a session. 341 | * 342 | * @param null|int $ttl 343 | * @return SessionManager 344 | */ 345 | public function rememberMe($ttl = null) 346 | { 347 | if (null === $ttl) { 348 | $ttl = $this->getConfig()->getRememberMeSeconds(); 349 | } 350 | $this->setSessionCookieLifetime($ttl); 351 | return $this; 352 | } 353 | 354 | /** 355 | * Set a 0s TTL for the session cookie 356 | * 357 | * Can safely be called in the middle of a session. 358 | * 359 | * @return SessionManager 360 | */ 361 | public function forgetMe() 362 | { 363 | $this->setSessionCookieLifetime(0); 364 | return $this; 365 | } 366 | 367 | /** 368 | * Set the validator chain to use when validating a session 369 | * 370 | * In most cases, you should use an instance of {@link ValidatorChain}. 371 | * 372 | * @param EventManagerInterface $chain 373 | * @return SessionManager 374 | */ 375 | public function setValidatorChain(EventManagerInterface $chain) 376 | { 377 | $this->validatorChain = $chain; 378 | return $this; 379 | } 380 | 381 | /** 382 | * Get the validator chain to use when validating a session 383 | * 384 | * By default, uses an instance of {@link ValidatorChain}. 385 | * 386 | * @return EventManagerInterface 387 | */ 388 | public function getValidatorChain() 389 | { 390 | if (null === $this->validatorChain) { 391 | $this->setValidatorChain(new ValidatorChain($this->getStorage())); 392 | } 393 | return $this->validatorChain; 394 | } 395 | 396 | /** 397 | * Is this session valid? 398 | * 399 | * Notifies the Validator Chain until either all validators have returned 400 | * true or one has failed. 401 | * 402 | * @return bool 403 | */ 404 | public function isValid() 405 | { 406 | $validator = $this->getValidatorChain(); 407 | 408 | $event = new Event(); 409 | $event->setName('session.validate'); 410 | $event->setTarget($this); 411 | $event->setParams($this); 412 | 413 | $falseResult = function ($test) { 414 | return false === $test; 415 | }; 416 | 417 | $responses = $validator->triggerEventUntil($falseResult, $event); 418 | 419 | if ($responses->stopped()) { 420 | // If execution was halted, validation failed 421 | return false; 422 | } 423 | 424 | // Otherwise, we're good to go 425 | return true; 426 | } 427 | 428 | /** 429 | * Expire the session cookie 430 | * 431 | * Sends a session cookie with no value, and with an expiry in the past. 432 | * 433 | * @return void 434 | */ 435 | public function expireSessionCookie() 436 | { 437 | $config = $this->getConfig(); 438 | if (! $config->getUseCookies()) { 439 | return; 440 | } 441 | setcookie( 442 | $this->getName(), // session name 443 | '', // value 444 | $_SERVER['REQUEST_TIME'] - 42000, // TTL for cookie 445 | $config->getCookiePath(), 446 | $config->getCookieDomain(), 447 | $config->getCookieSecure(), 448 | $config->getCookieHttpOnly() 449 | ); 450 | } 451 | 452 | /** 453 | * Set the session cookie lifetime 454 | * 455 | * If a session already exists, destroys it (without sending an expiration 456 | * cookie), regenerates the session ID, and restarts the session. 457 | * 458 | * @param int $ttl 459 | * @return void 460 | */ 461 | protected function setSessionCookieLifetime($ttl) 462 | { 463 | $config = $this->getConfig(); 464 | if (! $config->getUseCookies()) { 465 | return; 466 | } 467 | 468 | // Set new cookie TTL 469 | $config->setCookieLifetime($ttl); 470 | 471 | if ($this->sessionExists()) { 472 | // There is a running session so we'll regenerate id to send a new cookie 473 | $this->regenerateId(); 474 | } 475 | } 476 | 477 | /** 478 | * Register Save Handler with ext/session 479 | * 480 | * Since ext/session is coupled to this particular session manager 481 | * register the save handler with ext/session. 482 | * 483 | * @param SaveHandler\SaveHandlerInterface $saveHandler 484 | * @return bool 485 | */ 486 | protected function registerSaveHandler(SaveHandler\SaveHandlerInterface $saveHandler) 487 | { 488 | return session_set_save_handler($saveHandler); 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /src/Storage/AbstractSessionArrayStorage.php: -------------------------------------------------------------------------------- 1 | init($input); 34 | } 35 | 36 | /** 37 | * Initialize Storage 38 | * 39 | * @param array $input 40 | * @return void 41 | */ 42 | public function init($input = null) 43 | { 44 | if ((null === $input) && isset($_SESSION)) { 45 | $input = $_SESSION; 46 | if (is_object($input) && ! $_SESSION instanceof \ArrayObject) { 47 | $input = (array) $input; 48 | } 49 | } elseif (null === $input) { 50 | $input = []; 51 | } 52 | $_SESSION = $input; 53 | $this->setRequestAccessTime(microtime(true)); 54 | } 55 | 56 | /** 57 | * Get Offset 58 | * 59 | * @param mixed $key 60 | * @return mixed 61 | */ 62 | public function __get($key) 63 | { 64 | return $this->offsetGet($key); 65 | } 66 | 67 | /** 68 | * Set Offset 69 | * 70 | * @param mixed $key 71 | * @param mixed $value 72 | * @return void 73 | */ 74 | public function __set($key, $value) 75 | { 76 | return $this->offsetSet($key, $value); 77 | } 78 | 79 | /** 80 | * Isset Offset 81 | * 82 | * @param mixed $key 83 | * @return bool 84 | */ 85 | public function __isset($key) 86 | { 87 | return $this->offsetExists($key); 88 | } 89 | 90 | /** 91 | * Unset Offset 92 | * 93 | * @param mixed $key 94 | * @return void 95 | */ 96 | public function __unset($key) 97 | { 98 | return $this->offsetUnset($key); 99 | } 100 | 101 | /** 102 | * Destructor 103 | * 104 | * @return void 105 | */ 106 | public function __destruct() 107 | { 108 | return ; 109 | } 110 | 111 | /** 112 | * Offset Exists 113 | * 114 | * @param mixed $key 115 | * @return bool 116 | */ 117 | public function offsetExists($key) 118 | { 119 | return isset($_SESSION[$key]); 120 | } 121 | 122 | /** 123 | * Offset Get 124 | * 125 | * @param mixed $key 126 | * @return mixed 127 | */ 128 | public function offsetGet($key) 129 | { 130 | if (isset($_SESSION[$key])) { 131 | return $_SESSION[$key]; 132 | } 133 | 134 | return; 135 | } 136 | 137 | /** 138 | * Offset Set 139 | * 140 | * @param mixed $key 141 | * @param mixed $value 142 | * @return void 143 | */ 144 | public function offsetSet($key, $value) 145 | { 146 | $_SESSION[$key] = $value; 147 | } 148 | 149 | /** 150 | * Offset Unset 151 | * 152 | * @param mixed $key 153 | * @return void 154 | */ 155 | public function offsetUnset($key) 156 | { 157 | unset($_SESSION[$key]); 158 | } 159 | 160 | /** 161 | * Count 162 | * 163 | * @return int 164 | */ 165 | public function count() 166 | { 167 | return count($_SESSION); 168 | } 169 | 170 | /** 171 | * Seralize 172 | * 173 | * @return string 174 | */ 175 | public function serialize() 176 | { 177 | return serialize($_SESSION); 178 | } 179 | 180 | /** 181 | * Unserialize 182 | * 183 | * @param string $session 184 | * @return mixed 185 | */ 186 | public function unserialize($session) 187 | { 188 | return unserialize($session); 189 | } 190 | 191 | /** 192 | * Get Iterator 193 | * 194 | * @return ArrayIterator 195 | */ 196 | public function getIterator() 197 | { 198 | return new ArrayIterator($_SESSION); 199 | } 200 | 201 | /** 202 | * Load session object from an existing array 203 | * 204 | * Ensures $_SESSION is set to an instance of the object when complete. 205 | * 206 | * @param array $array 207 | * @return SessionStorage 208 | */ 209 | public function fromArray(array $array) 210 | { 211 | $ts = $this->getRequestAccessTime(); 212 | $_SESSION = $array; 213 | $this->setRequestAccessTime($ts); 214 | 215 | return $this; 216 | } 217 | 218 | /** 219 | * Mark object as isImmutable 220 | * 221 | * @return SessionStorage 222 | */ 223 | public function markImmutable() 224 | { 225 | $_SESSION['_IMMUTABLE'] = true; 226 | 227 | return $this; 228 | } 229 | 230 | /** 231 | * Determine if this object is isImmutable 232 | * 233 | * @return bool 234 | */ 235 | public function isImmutable() 236 | { 237 | return (isset($_SESSION['_IMMUTABLE']) && $_SESSION['_IMMUTABLE']); 238 | } 239 | 240 | /** 241 | * Lock this storage instance, or a key within it 242 | * 243 | * @param null|int|string $key 244 | * @return ArrayStorage 245 | */ 246 | public function lock($key = null) 247 | { 248 | if (null === $key) { 249 | $this->setMetadata('_READONLY', true); 250 | 251 | return $this; 252 | } 253 | if (isset($_SESSION[$key])) { 254 | $this->setMetadata('_LOCKS', [$key => true]); 255 | } 256 | 257 | return $this; 258 | } 259 | 260 | /** 261 | * Is the object or key marked as locked? 262 | * 263 | * @param null|int|string $key 264 | * @return bool 265 | */ 266 | public function isLocked($key = null) 267 | { 268 | if ($this->isImmutable()) { 269 | // isImmutable trumps all 270 | return true; 271 | } 272 | 273 | if (null === $key) { 274 | // testing for global lock 275 | return $this->getMetadata('_READONLY'); 276 | } 277 | 278 | $locks = $this->getMetadata('_LOCKS'); 279 | $readOnly = $this->getMetadata('_READONLY'); 280 | 281 | if ($readOnly && ! $locks) { 282 | // global lock in play; all keys are locked 283 | return true; 284 | } 285 | if ($readOnly && $locks) { 286 | return array_key_exists($key, $locks); 287 | } 288 | 289 | // test for individual locks 290 | if (! $locks) { 291 | return false; 292 | } 293 | 294 | return array_key_exists($key, $locks); 295 | } 296 | 297 | /** 298 | * Unlock an object or key marked as locked 299 | * 300 | * @param null|int|string $key 301 | * @return ArrayStorage 302 | */ 303 | public function unlock($key = null) 304 | { 305 | if (null === $key) { 306 | // Unlock everything 307 | $this->setMetadata('_READONLY', false); 308 | $this->setMetadata('_LOCKS', false); 309 | 310 | return $this; 311 | } 312 | 313 | $locks = $this->getMetadata('_LOCKS'); 314 | if (! $locks) { 315 | if (! $this->getMetadata('_READONLY')) { 316 | return $this; 317 | } 318 | $array = $this->toArray(); 319 | $keys = array_keys($array); 320 | $locks = array_flip($keys); 321 | unset($array, $keys); 322 | } 323 | 324 | if (array_key_exists($key, $locks)) { 325 | unset($locks[$key]); 326 | $this->setMetadata('_LOCKS', $locks, true); 327 | } 328 | 329 | return $this; 330 | } 331 | 332 | /** 333 | * Set storage metadata 334 | * 335 | * Metadata is used to store information about the data being stored in the 336 | * object. Some example use cases include: 337 | * - Setting expiry data 338 | * - Maintaining access counts 339 | * - localizing session storage 340 | * - etc. 341 | * 342 | * @param string $key 343 | * @param mixed $value 344 | * @param bool $overwriteArray Whether to overwrite or merge array values; by default, merges 345 | * @return ArrayStorage 346 | * @throws Exception\RuntimeException 347 | */ 348 | public function setMetadata($key, $value, $overwriteArray = false) 349 | { 350 | if ($this->isImmutable()) { 351 | throw new Exception\RuntimeException( 352 | sprintf('Cannot set key "%s" as storage is marked isImmutable', $key) 353 | ); 354 | } 355 | 356 | if (! isset($_SESSION['__ZF']) || ! is_array($_SESSION['__ZF'])) { 357 | $_SESSION['__ZF'] = []; 358 | } 359 | if (isset($_SESSION['__ZF'][$key]) && is_array($value)) { 360 | if ($overwriteArray) { 361 | $_SESSION['__ZF'][$key] = $value; 362 | } else { 363 | $_SESSION['__ZF'][$key] = array_replace_recursive($_SESSION['__ZF'][$key], $value); 364 | } 365 | } else { 366 | if ((null === $value) && isset($_SESSION['__ZF'][$key])) { 367 | $array = $_SESSION['__ZF']; 368 | unset($array[$key]); 369 | $_SESSION['__ZF'] = $array; 370 | unset($array); 371 | } elseif (null !== $value) { 372 | $_SESSION['__ZF'][$key] = $value; 373 | } 374 | } 375 | 376 | return $this; 377 | } 378 | 379 | /** 380 | * Retrieve metadata for the storage object or a specific metadata key 381 | * 382 | * Returns false if no metadata stored, or no metadata exists for the given 383 | * key. 384 | * 385 | * @param null|int|string $key 386 | * @return mixed 387 | */ 388 | public function getMetadata($key = null) 389 | { 390 | if (! isset($_SESSION['__ZF'])) { 391 | return false; 392 | } 393 | 394 | if (null === $key) { 395 | return $_SESSION['__ZF']; 396 | } 397 | 398 | if (! array_key_exists($key, $_SESSION['__ZF'])) { 399 | return false; 400 | } 401 | 402 | return $_SESSION['__ZF'][$key]; 403 | } 404 | 405 | /** 406 | * Clear the storage object or a subkey of the object 407 | * 408 | * @param null|int|string $key 409 | * @return ArrayStorage 410 | * @throws Exception\RuntimeException 411 | */ 412 | public function clear($key = null) 413 | { 414 | if ($this->isImmutable()) { 415 | throw new Exception\RuntimeException('Cannot clear storage as it is marked immutable'); 416 | } 417 | if (null === $key) { 418 | $this->fromArray([]); 419 | 420 | return $this; 421 | } 422 | 423 | if (! isset($_SESSION[$key])) { 424 | return $this; 425 | } 426 | 427 | // Clear key data 428 | unset($_SESSION[$key]); 429 | 430 | // Clear key metadata 431 | $this->setMetadata($key, null) 432 | ->unlock($key); 433 | 434 | return $this; 435 | } 436 | 437 | /** 438 | * Retrieve the request access time 439 | * 440 | * @return float 441 | */ 442 | public function getRequestAccessTime() 443 | { 444 | return $this->getMetadata('_REQUEST_ACCESS_TIME'); 445 | } 446 | 447 | /** 448 | * Set the request access time 449 | * 450 | * @param float $time 451 | * @return ArrayStorage 452 | */ 453 | protected function setRequestAccessTime($time) 454 | { 455 | $this->setMetadata('_REQUEST_ACCESS_TIME', $time); 456 | 457 | return $this; 458 | } 459 | 460 | /** 461 | * Cast the object to an array 462 | * 463 | * @param bool $metaData Whether to include metadata 464 | * @return array 465 | */ 466 | public function toArray($metaData = false) 467 | { 468 | if (isset($_SESSION)) { 469 | $values = $_SESSION; 470 | } else { 471 | $values = []; 472 | } 473 | 474 | if ($metaData) { 475 | return $values; 476 | } 477 | 478 | if (isset($values['__ZF'])) { 479 | unset($values['__ZF']); 480 | } 481 | 482 | return $values; 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /src/Storage/ArrayStorage.php: -------------------------------------------------------------------------------- 1 | setRequestAccessTime(microtime(true)); 44 | } 45 | 46 | /** 47 | * Set the request access time 48 | * 49 | * @param float $time 50 | * @return ArrayStorage 51 | */ 52 | protected function setRequestAccessTime($time) 53 | { 54 | $this->setMetadata('_REQUEST_ACCESS_TIME', $time); 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * Retrieve the request access time 61 | * 62 | * @return float 63 | */ 64 | public function getRequestAccessTime() 65 | { 66 | return $this->getMetadata('_REQUEST_ACCESS_TIME'); 67 | } 68 | 69 | /** 70 | * Set a value in the storage object 71 | * 72 | * If the object is marked as isImmutable, or the object or key is marked as 73 | * locked, raises an exception. 74 | * 75 | * @param string $key 76 | * @param mixed $value 77 | * @return void 78 | */ 79 | 80 | /** 81 | * @param mixed $key 82 | * @param mixed $value 83 | * @throws Exception\RuntimeException 84 | */ 85 | public function offsetSet($key, $value) 86 | { 87 | if ($this->isImmutable()) { 88 | throw new Exception\RuntimeException( 89 | sprintf('Cannot set key "%s" as storage is marked isImmutable', $key) 90 | ); 91 | } 92 | if ($this->isLocked($key)) { 93 | throw new Exception\RuntimeException( 94 | sprintf('Cannot set key "%s" due to locking', $key) 95 | ); 96 | } 97 | 98 | parent::offsetSet($key, $value); 99 | } 100 | 101 | /** 102 | * Lock this storage instance, or a key within it 103 | * 104 | * @param null|int|string $key 105 | * @return ArrayStorage 106 | */ 107 | public function lock($key = null) 108 | { 109 | if (null === $key) { 110 | $this->setMetadata('_READONLY', true); 111 | 112 | return $this; 113 | } 114 | if (isset($this[$key])) { 115 | $this->setMetadata('_LOCKS', [$key => true]); 116 | } 117 | 118 | return $this; 119 | } 120 | 121 | /** 122 | * Is the object or key marked as locked? 123 | * 124 | * @param null|int|string $key 125 | * @return bool 126 | */ 127 | public function isLocked($key = null) 128 | { 129 | if ($this->isImmutable()) { 130 | // isImmutable trumps all 131 | return true; 132 | } 133 | 134 | if (null === $key) { 135 | // testing for global lock 136 | return $this->getMetadata('_READONLY'); 137 | } 138 | 139 | $locks = $this->getMetadata('_LOCKS'); 140 | $readOnly = $this->getMetadata('_READONLY'); 141 | 142 | if ($readOnly && ! $locks) { 143 | // global lock in play; all keys are locked 144 | return true; 145 | } elseif ($readOnly && $locks) { 146 | return array_key_exists($key, $locks); 147 | } 148 | 149 | // test for individual locks 150 | if (! $locks) { 151 | return false; 152 | } 153 | 154 | return array_key_exists($key, $locks); 155 | } 156 | 157 | /** 158 | * Unlock an object or key marked as locked 159 | * 160 | * @param null|int|string $key 161 | * @return ArrayStorage 162 | */ 163 | public function unlock($key = null) 164 | { 165 | if (null === $key) { 166 | // Unlock everything 167 | $this->setMetadata('_READONLY', false); 168 | $this->setMetadata('_LOCKS', false); 169 | 170 | return $this; 171 | } 172 | 173 | $locks = $this->getMetadata('_LOCKS'); 174 | if (! $locks) { 175 | if (! $this->getMetadata('_READONLY')) { 176 | return $this; 177 | } 178 | $array = $this->toArray(); 179 | $keys = array_keys($array); 180 | $locks = array_flip($keys); 181 | unset($array, $keys); 182 | } 183 | 184 | if (array_key_exists($key, $locks)) { 185 | unset($locks[$key]); 186 | $this->setMetadata('_LOCKS', $locks, true); 187 | } 188 | 189 | return $this; 190 | } 191 | 192 | /** 193 | * Mark the storage container as isImmutable 194 | * 195 | * @return ArrayStorage 196 | */ 197 | public function markImmutable() 198 | { 199 | $this->isImmutable = true; 200 | 201 | return $this; 202 | } 203 | 204 | /** 205 | * Is the storage container marked as isImmutable? 206 | * 207 | * @return bool 208 | */ 209 | public function isImmutable() 210 | { 211 | return $this->isImmutable; 212 | } 213 | 214 | /** 215 | * Set storage metadata 216 | * 217 | * Metadata is used to store information about the data being stored in the 218 | * object. Some example use cases include: 219 | * - Setting expiry data 220 | * - Maintaining access counts 221 | * - localizing session storage 222 | * - etc. 223 | * 224 | * @param string $key 225 | * @param mixed $value 226 | * @param bool $overwriteArray Whether to overwrite or merge array values; by default, merges 227 | * @return ArrayStorage 228 | * @throws Exception\RuntimeException 229 | */ 230 | public function setMetadata($key, $value, $overwriteArray = false) 231 | { 232 | if ($this->isImmutable) { 233 | throw new Exception\RuntimeException( 234 | sprintf('Cannot set key "%s" as storage is marked isImmutable', $key) 235 | ); 236 | } 237 | 238 | if (! isset($this['__ZF'])) { 239 | $this['__ZF'] = []; 240 | } 241 | 242 | if (isset($this['__ZF'][$key]) && is_array($value)) { 243 | if ($overwriteArray) { 244 | $this['__ZF'][$key] = $value; 245 | } else { 246 | $this['__ZF'][$key] = array_replace_recursive($this['__ZF'][$key], $value); 247 | } 248 | } else { 249 | if ((null === $value) && isset($this['__ZF'][$key])) { 250 | // unset($this['__ZF'][$key]) led to "indirect modification... 251 | // has no effect" errors, so explicitly pulling array and 252 | // unsetting key. 253 | $array = $this['__ZF']; 254 | unset($array[$key]); 255 | $this['__ZF'] = $array; 256 | unset($array); 257 | } elseif (null !== $value) { 258 | $this['__ZF'][$key] = $value; 259 | } 260 | } 261 | 262 | return $this; 263 | } 264 | 265 | /** 266 | * Retrieve metadata for the storage object or a specific metadata key 267 | * 268 | * Returns false if no metadata stored, or no metadata exists for the given 269 | * key. 270 | * 271 | * @param null|int|string $key 272 | * @return mixed 273 | */ 274 | public function getMetadata($key = null) 275 | { 276 | if (! isset($this['__ZF'])) { 277 | return false; 278 | } 279 | 280 | if (null === $key) { 281 | return $this['__ZF']; 282 | } 283 | 284 | if (! array_key_exists($key, $this['__ZF'])) { 285 | return false; 286 | } 287 | 288 | return $this['__ZF'][$key]; 289 | } 290 | 291 | /** 292 | * Clear the storage object or a subkey of the object 293 | * 294 | * @param null|int|string $key 295 | * @return ArrayStorage 296 | * @throws Exception\RuntimeException 297 | */ 298 | public function clear($key = null) 299 | { 300 | if ($this->isImmutable()) { 301 | throw new Exception\RuntimeException('Cannot clear storage as it is marked immutable'); 302 | } 303 | if (null === $key) { 304 | $this->fromArray([]); 305 | 306 | return $this; 307 | } 308 | 309 | if (! isset($this[$key])) { 310 | return $this; 311 | } 312 | 313 | // Clear key data 314 | unset($this[$key]); 315 | 316 | // Clear key metadata 317 | $this->setMetadata($key, null) 318 | ->unlock($key); 319 | 320 | return $this; 321 | } 322 | 323 | /** 324 | * Load the storage from another array 325 | * 326 | * Overwrites any data that was previously set. 327 | * 328 | * @param array $array 329 | * @return ArrayStorage 330 | */ 331 | public function fromArray(array $array) 332 | { 333 | $ts = $this->getRequestAccessTime(); 334 | $this->exchangeArray($array); 335 | $this->setRequestAccessTime($ts); 336 | 337 | return $this; 338 | } 339 | 340 | /** 341 | * Cast the object to an array 342 | * 343 | * @param bool $metaData Whether to include metadata 344 | * @return array 345 | */ 346 | public function toArray($metaData = false) 347 | { 348 | $values = $this->getArrayCopy(); 349 | if ($metaData) { 350 | return $values; 351 | } 352 | if (isset($values['__ZF'])) { 353 | unset($values['__ZF']); 354 | } 355 | 356 | return $values; 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/Storage/Factory.php: -------------------------------------------------------------------------------- 1 | getArrayCopy(); 61 | } 62 | 63 | /** 64 | * Load session object from an existing array 65 | * 66 | * Ensures $_SESSION is set to an instance of the object when complete. 67 | * 68 | * @param array $array 69 | * @return SessionStorage 70 | */ 71 | public function fromArray(array $array) 72 | { 73 | parent::fromArray($array); 74 | if ($_SESSION !== $this) { 75 | $_SESSION = $this; 76 | } 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * Mark object as isImmutable 83 | * 84 | * @return SessionStorage 85 | */ 86 | public function markImmutable() 87 | { 88 | $this['_IMMUTABLE'] = true; 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * Determine if this object is isImmutable 95 | * 96 | * @return bool 97 | */ 98 | public function isImmutable() 99 | { 100 | return (isset($this['_IMMUTABLE']) && $this['_IMMUTABLE']); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Storage/StorageInitializationInterface.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 34 | $validators = $storage->getMetadata('_VALID'); 35 | if ($validators) { 36 | foreach ($validators as $validator => $data) { 37 | $this->attachValidator('session.validate', [new $validator($data), 'isValid'], 1); 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Attach a listener to the session validator chain. 44 | * 45 | * @param string $event 46 | * @param null|callable $callback 47 | * @param int $priority 48 | * @return \Zend\Stdlib\CallbackHandler 49 | */ 50 | public function attach($event, $callback = null, $priority = 1) 51 | { 52 | return $this->attachValidator($event, $callback, $priority); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Validator/AbstractValidatorChainEM3.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 34 | $validators = $storage->getMetadata('_VALID'); 35 | if ($validators) { 36 | foreach ($validators as $validator => $data) { 37 | $this->attachValidator('session.validate', [new $validator($data), 'isValid'], 1); 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Attach a listener to the session validator chain. 44 | * 45 | * @param string $eventName 46 | * @param callable $callback 47 | * @param int $priority 48 | * @return \Zend\Stdlib\CallbackHandler 49 | */ 50 | public function attach($eventName, callable $callback, $priority = 1) 51 | { 52 | return $this->attachValidator($eventName, $callback, $priority); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Validator/HttpUserAgent.php: -------------------------------------------------------------------------------- 1 | data = $data; 33 | } 34 | 35 | /** 36 | * isValid() - this method will determine if the current user agent matches the 37 | * user agent we stored when we initialized this variable. 38 | * 39 | * @return bool 40 | */ 41 | public function isValid() 42 | { 43 | $userAgent = isset($_SERVER['HTTP_USER_AGENT']) 44 | ? $_SERVER['HTTP_USER_AGENT'] 45 | : null; 46 | 47 | return ($userAgent === $this->getData()); 48 | } 49 | 50 | /** 51 | * Retrieve token for validating call 52 | * 53 | * @return string 54 | */ 55 | public function getData() 56 | { 57 | return $this->data; 58 | } 59 | 60 | /** 61 | * Return validator name 62 | * 63 | * @return string 64 | */ 65 | public function getName() 66 | { 67 | return __CLASS__; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Validator/Id.php: -------------------------------------------------------------------------------- 1 | id = $id; 37 | } 38 | 39 | /** 40 | * Is the current session identifier valid? 41 | * 42 | * Tests that the identifier does not contain invalid characters. 43 | * 44 | * @return bool 45 | */ 46 | public function isValid() 47 | { 48 | $id = $this->id; 49 | $saveHandler = ini_get('session.save_handler'); 50 | if ($saveHandler == 'cluster') { // Zend Server SC, validate only after last dash 51 | $dashPos = strrpos($id, '-'); 52 | if ($dashPos) { 53 | $id = substr($id, $dashPos + 1); 54 | } 55 | } 56 | 57 | // Get the session id bits per character INI setting, using 5 if unavailable 58 | $bitsPerCharacter = PHP_VERSION_ID >= 70100 59 | ? 'session.sid_bits_per_character' 60 | : 'session.hash_bits_per_character'; 61 | $hashBitsPerChar = ini_get($bitsPerCharacter) ?: 5; 62 | 63 | switch ($hashBitsPerChar) { 64 | case 4: 65 | $pattern = '#^[0-9a-f]*$#'; 66 | break; 67 | case 6: 68 | $pattern = '#^[0-9a-zA-Z-,]*$#'; 69 | break; 70 | case 5: 71 | // intentionally fall-through 72 | default: 73 | $pattern = '#^[0-9a-v]*$#'; 74 | break; 75 | } 76 | 77 | return (bool) preg_match($pattern, $id); 78 | } 79 | 80 | /** 81 | * Retrieve token for validating call (session_id) 82 | * 83 | * @return string 84 | */ 85 | public function getData() 86 | { 87 | return $this->id; 88 | } 89 | 90 | /** 91 | * Return validator name 92 | * 93 | * @return string 94 | */ 95 | public function getName() 96 | { 97 | return __CLASS__; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Validator/RemoteAddr.php: -------------------------------------------------------------------------------- 1 | getIpAddress(); 58 | } 59 | $this->data = $data; 60 | } 61 | 62 | /** 63 | * isValid() - this method will determine if the current user IP matches the 64 | * IP we stored when we initialized this variable. 65 | * 66 | * @return bool 67 | */ 68 | public function isValid() 69 | { 70 | return ($this->getIpAddress() === $this->getData()); 71 | } 72 | 73 | /** 74 | * Changes proxy handling setting. 75 | * 76 | * This must be static method, since validators are recovered automatically 77 | * at session read, so this is the only way to switch setting. 78 | * 79 | * @param bool $useProxy Whether to check also proxied IP addresses. 80 | * @return void 81 | */ 82 | public static function setUseProxy($useProxy = true) 83 | { 84 | static::$useProxy = $useProxy; 85 | } 86 | 87 | /** 88 | * Checks proxy handling setting. 89 | * 90 | * @return bool Current setting value. 91 | */ 92 | public static function getUseProxy() 93 | { 94 | return static::$useProxy; 95 | } 96 | 97 | /** 98 | * Set list of trusted proxy addresses 99 | * 100 | * @param array $trustedProxies 101 | * @return void 102 | */ 103 | public static function setTrustedProxies(array $trustedProxies) 104 | { 105 | static::$trustedProxies = $trustedProxies; 106 | } 107 | 108 | /** 109 | * Set the header to introspect for proxy IPs 110 | * 111 | * @param string $header 112 | * @return void 113 | */ 114 | public static function setProxyHeader($header = 'X-Forwarded-For') 115 | { 116 | static::$proxyHeader = $header; 117 | } 118 | 119 | /** 120 | * Returns client IP address. 121 | * 122 | * @return string IP address. 123 | */ 124 | protected function getIpAddress() 125 | { 126 | $remoteAddress = new RemoteAddress(); 127 | $remoteAddress->setUseProxy(static::$useProxy); 128 | $remoteAddress->setTrustedProxies(static::$trustedProxies); 129 | $remoteAddress->setProxyHeader(static::$proxyHeader); 130 | return $remoteAddress->getIpAddress(); 131 | } 132 | 133 | /** 134 | * Retrieve token for validating call 135 | * 136 | * @return string 137 | */ 138 | public function getData() 139 | { 140 | return $this->data; 141 | } 142 | 143 | /** 144 | * Return validator name 145 | * 146 | * @return string 147 | */ 148 | public function getName() 149 | { 150 | return __CLASS__; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Validator/ValidatorChainTrait.php: -------------------------------------------------------------------------------- 1 | storage; 30 | } 31 | 32 | /** 33 | * Internal implementation for attaching a listener to the 34 | * session validator chain. 35 | * 36 | * @param string $event 37 | * @param callable $callback 38 | * @param int $priority 39 | * @return \Zend\Stdlib\CallbackHandler|callable 40 | */ 41 | private function attachValidator($event, $callback, $priority) 42 | { 43 | $context = null; 44 | if ($callback instanceof ValidatorInterface) { 45 | $context = $callback; 46 | } elseif (is_array($callback)) { 47 | $test = array_shift($callback); 48 | if ($test instanceof ValidatorInterface) { 49 | $context = $test; 50 | } 51 | array_unshift($callback, $test); 52 | } 53 | if ($context instanceof ValidatorInterface) { 54 | $data = $context->getData(); 55 | $name = $context->getName(); 56 | $this->getStorage()->setMetadata('_VALID', [$name => $data]); 57 | } 58 | 59 | $listener = parent::attach($event, $callback, $priority); 60 | return $listener; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Validator/ValidatorInterface.php: -------------------------------------------------------------------------------- 1 |