1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Session; 11 12use Zend\EventManager\EventManagerInterface; 13use Zend\Stdlib\ArrayUtils; 14 15/** 16 * Session ManagerInterface implementation utilizing ext/session 17 */ 18class SessionManager extends AbstractManager 19{ 20 /** 21 * Default options when a call to {@link destroy()} is made 22 * - send_expire_cookie: whether or not to send a cookie expiring the current session cookie 23 * - clear_storage: whether or not to empty the storage object of any stored values 24 * @var array 25 */ 26 protected $defaultDestroyOptions = array( 27 'send_expire_cookie' => true, 28 'clear_storage' => false, 29 ); 30 31 /** 32 * @var string value returned by session_name() 33 */ 34 protected $name; 35 36 /** 37 * @var EventManagerInterface Validation chain to determine if session is valid 38 */ 39 protected $validatorChain; 40 41 /** 42 * Constructor 43 * 44 * @param Config\ConfigInterface|null $config 45 * @param Storage\StorageInterface|null $storage 46 * @param SaveHandler\SaveHandlerInterface|null $saveHandler 47 * @param array $validators 48 * @throws Exception\RuntimeException 49 */ 50 public function __construct( 51 Config\ConfigInterface $config = null, 52 Storage\StorageInterface $storage = null, 53 SaveHandler\SaveHandlerInterface $saveHandler = null, 54 array $validators = array() 55 ) { 56 parent::__construct($config, $storage, $saveHandler, $validators); 57 register_shutdown_function(array($this, 'writeClose')); 58 } 59 60 /** 61 * Does a session exist and is it currently active? 62 * 63 * @return bool 64 */ 65 public function sessionExists() 66 { 67 $sid = defined('SID') ? constant('SID') : false; 68 if ($sid !== false && $this->getId()) { 69 return true; 70 } 71 if (headers_sent()) { 72 return true; 73 } 74 return false; 75 } 76 77 /** 78 * Start session 79 * 80 * if No session currently exists, attempt to start it. Calls 81 * {@link isValid()} once session_start() is called, and raises an 82 * exception if validation fails. 83 * 84 * @param bool $preserveStorage If set to true, current session storage will not be overwritten by the 85 * contents of $_SESSION. 86 * @return void 87 * @throws Exception\RuntimeException 88 */ 89 public function start($preserveStorage = false) 90 { 91 if ($this->sessionExists()) { 92 return; 93 } 94 95 $saveHandler = $this->getSaveHandler(); 96 if ($saveHandler instanceof SaveHandler\SaveHandlerInterface) { 97 // register the session handler with ext/session 98 $this->registerSaveHandler($saveHandler); 99 } 100 101 $oldSessionData = array(); 102 if (isset($_SESSION)) { 103 $oldSessionData = $_SESSION; 104 } 105 106 session_start(); 107 108 if ($oldSessionData instanceof \Traversable 109 || (! empty($oldSessionData) && is_array($oldSessionData)) 110 ) { 111 $_SESSION = ArrayUtils::merge($oldSessionData, $_SESSION, true); 112 } 113 114 $storage = $this->getStorage(); 115 116 // Since session is starting, we need to potentially repopulate our 117 // session storage 118 if ($storage instanceof Storage\SessionStorage && $_SESSION !== $storage) { 119 if (!$preserveStorage) { 120 $storage->fromArray($_SESSION); 121 } 122 $_SESSION = $storage; 123 } elseif ($storage instanceof Storage\StorageInitializationInterface) { 124 $storage->init($_SESSION); 125 } 126 127 $this->initializeValidatorChain(); 128 129 if (!$this->isValid()) { 130 throw new Exception\RuntimeException('Session validation failed'); 131 } 132 } 133 134 /** 135 * Create validators, insert reference value and add them to the validator chain 136 */ 137 protected function initializeValidatorChain() 138 { 139 $validatorChain = $this->getValidatorChain(); 140 $validatorValues = $this->getStorage()->getMetadata('_VALID'); 141 142 foreach ($this->validators as $validator) { 143 // Ignore validators which are already present in Storage 144 if (is_array($validatorValues) && array_key_exists($validator, $validatorValues)) { 145 continue; 146 } 147 148 $validator = new $validator(null); 149 $validatorChain->attach('session.validate', array($validator, 'isValid')); 150 } 151 } 152 153 /** 154 * Destroy/end a session 155 * 156 * @param array $options See {@link $defaultDestroyOptions} 157 * @return void 158 */ 159 public function destroy(array $options = null) 160 { 161 if (!$this->sessionExists()) { 162 return; 163 } 164 165 if (null === $options) { 166 $options = $this->defaultDestroyOptions; 167 } else { 168 $options = array_merge($this->defaultDestroyOptions, $options); 169 } 170 171 session_destroy(); 172 if ($options['send_expire_cookie']) { 173 $this->expireSessionCookie(); 174 } 175 176 if ($options['clear_storage']) { 177 $this->getStorage()->clear(); 178 } 179 } 180 181 /** 182 * Write session to save handler and close 183 * 184 * Once done, the Storage object will be marked as isImmutable. 185 * 186 * @return void 187 */ 188 public function writeClose() 189 { 190 // The assumption is that we're using PHP's ext/session. 191 // session_write_close() will actually overwrite $_SESSION with an 192 // empty array on completion -- which leads to a mismatch between what 193 // is in the storage object and $_SESSION. To get around this, we 194 // temporarily reset $_SESSION to an array, and then re-link it to 195 // the storage object. 196 // 197 // Additionally, while you _can_ write to $_SESSION following a 198 // session_write_close() operation, no changes made to it will be 199 // flushed to the session handler. As such, we now mark the storage 200 // object isImmutable. 201 $storage = $this->getStorage(); 202 if (!$storage->isImmutable()) { 203 $_SESSION = $storage->toArray(true); 204 session_write_close(); 205 $storage->fromArray($_SESSION); 206 $storage->markImmutable(); 207 } 208 } 209 210 /** 211 * Attempt to set the session name 212 * 213 * If the session has already been started, or if the name provided fails 214 * validation, an exception will be raised. 215 * 216 * @param string $name 217 * @return SessionManager 218 * @throws Exception\InvalidArgumentException 219 */ 220 public function setName($name) 221 { 222 if ($this->sessionExists()) { 223 throw new Exception\InvalidArgumentException( 224 'Cannot set session name after a session has already started' 225 ); 226 } 227 228 if (!preg_match('/^[a-zA-Z0-9]+$/', $name)) { 229 throw new Exception\InvalidArgumentException( 230 'Name provided contains invalid characters; must be alphanumeric only' 231 ); 232 } 233 234 $this->name = $name; 235 session_name($name); 236 return $this; 237 } 238 239 /** 240 * Get session name 241 * 242 * Proxies to {@link session_name()}. 243 * 244 * @return string 245 */ 246 public function getName() 247 { 248 if (null === $this->name) { 249 // If we're grabbing via session_name(), we don't need our 250 // validation routine; additionally, calling setName() after 251 // session_start() can lead to issues, and often we just need the name 252 // in order to do things such as setting cookies. 253 $this->name = session_name(); 254 } 255 return $this->name; 256 } 257 258 /** 259 * Set session ID 260 * 261 * Can safely be called in the middle of a session. 262 * 263 * @param string $id 264 * @return SessionManager 265 */ 266 public function setId($id) 267 { 268 if ($this->sessionExists()) { 269 throw new Exception\RuntimeException('Session has already been started, to change the session ID call regenerateId()'); 270 } 271 session_id($id); 272 return $this; 273 } 274 275 /** 276 * Get session ID 277 * 278 * Proxies to {@link session_id()} 279 * 280 * @return string 281 */ 282 public function getId() 283 { 284 return session_id(); 285 } 286 287 /** 288 * Regenerate id 289 * 290 * Regenerate the session ID, using session save handler's 291 * native ID generation Can safely be called in the middle of a session. 292 * 293 * @param bool $deleteOldSession 294 * @return SessionManager 295 */ 296 public function regenerateId($deleteOldSession = true) 297 { 298 session_regenerate_id((bool) $deleteOldSession); 299 return $this; 300 } 301 302 /** 303 * Set the TTL (in seconds) for the session cookie expiry 304 * 305 * Can safely be called in the middle of a session. 306 * 307 * @param null|int $ttl 308 * @return SessionManager 309 */ 310 public function rememberMe($ttl = null) 311 { 312 if (null === $ttl) { 313 $ttl = $this->getConfig()->getRememberMeSeconds(); 314 } 315 $this->setSessionCookieLifetime($ttl); 316 return $this; 317 } 318 319 /** 320 * Set a 0s TTL for the session cookie 321 * 322 * Can safely be called in the middle of a session. 323 * 324 * @return SessionManager 325 */ 326 public function forgetMe() 327 { 328 $this->setSessionCookieLifetime(0); 329 return $this; 330 } 331 332 /** 333 * Set the validator chain to use when validating a session 334 * 335 * In most cases, you should use an instance of {@link ValidatorChain}. 336 * 337 * @param EventManagerInterface $chain 338 * @return SessionManager 339 */ 340 public function setValidatorChain(EventManagerInterface $chain) 341 { 342 $this->validatorChain = $chain; 343 return $this; 344 } 345 346 /** 347 * Get the validator chain to use when validating a session 348 * 349 * By default, uses an instance of {@link ValidatorChain}. 350 * 351 * @return EventManagerInterface 352 */ 353 public function getValidatorChain() 354 { 355 if (null === $this->validatorChain) { 356 $this->setValidatorChain(new ValidatorChain($this->getStorage())); 357 } 358 return $this->validatorChain; 359 } 360 361 /** 362 * Is this session valid? 363 * 364 * Notifies the Validator Chain until either all validators have returned 365 * true or one has failed. 366 * 367 * @return bool 368 */ 369 public function isValid() 370 { 371 $validator = $this->getValidatorChain(); 372 $responses = $validator->trigger('session.validate', $this, array($this), function ($test) { 373 return false === $test; 374 }); 375 if ($responses->stopped()) { 376 // If execution was halted, validation failed 377 return false; 378 } 379 // Otherwise, we're good to go 380 return true; 381 } 382 383 /** 384 * Expire the session cookie 385 * 386 * Sends a session cookie with no value, and with an expiry in the past. 387 * 388 * @return void 389 */ 390 public function expireSessionCookie() 391 { 392 $config = $this->getConfig(); 393 if (!$config->getUseCookies()) { 394 return; 395 } 396 setcookie( 397 $this->getName(), // session name 398 '', // value 399 $_SERVER['REQUEST_TIME'] - 42000, // TTL for cookie 400 $config->getCookiePath(), 401 $config->getCookieDomain(), 402 $config->getCookieSecure(), 403 $config->getCookieHttpOnly() 404 ); 405 } 406 407 /** 408 * Set the session cookie lifetime 409 * 410 * If a session already exists, destroys it (without sending an expiration 411 * cookie), regenerates the session ID, and restarts the session. 412 * 413 * @param int $ttl 414 * @return void 415 */ 416 protected function setSessionCookieLifetime($ttl) 417 { 418 $config = $this->getConfig(); 419 if (!$config->getUseCookies()) { 420 return; 421 } 422 423 // Set new cookie TTL 424 $config->setCookieLifetime($ttl); 425 426 if ($this->sessionExists()) { 427 // There is a running session so we'll regenerate id to send a new cookie 428 $this->regenerateId(); 429 } 430 } 431 432 /** 433 * Register Save Handler with ext/session 434 * 435 * Since ext/session is coupled to this particular session manager 436 * register the save handler with ext/session. 437 * 438 * @param SaveHandler\SaveHandlerInterface $saveHandler 439 * @return bool 440 */ 441 protected function registerSaveHandler(SaveHandler\SaveHandlerInterface $saveHandler) 442 { 443 return session_set_save_handler( 444 array($saveHandler, 'open'), 445 array($saveHandler, 'close'), 446 array($saveHandler, 'read'), 447 array($saveHandler, 'write'), 448 array($saveHandler, 'destroy'), 449 array($saveHandler, 'gc') 450 ); 451 } 452} 453