1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; 13 14use Symfony\Component\HttpFoundation\Session\SessionUtils; 15 16/** 17 * This abstract session handler provides a generic implementation 18 * of the PHP 7.0 SessionUpdateTimestampHandlerInterface, 19 * enabling strict and lazy session handling. 20 * 21 * @author Nicolas Grekas <p@tchwork.com> 22 */ 23abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface 24{ 25 private $sessionName; 26 private $prefetchId; 27 private $prefetchData; 28 private $newSessionId; 29 private $igbinaryEmptyData; 30 31 /** 32 * @return bool 33 */ 34 public function open($savePath, $sessionName) 35 { 36 $this->sessionName = $sessionName; 37 if (!headers_sent() && !ini_get('session.cache_limiter') && '0' !== ini_get('session.cache_limiter')) { 38 header(sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) ini_get('session.cache_expire'))); 39 } 40 41 return true; 42 } 43 44 /** 45 * @param string $sessionId 46 * 47 * @return string 48 */ 49 abstract protected function doRead($sessionId); 50 51 /** 52 * @param string $sessionId 53 * @param string $data 54 * 55 * @return bool 56 */ 57 abstract protected function doWrite($sessionId, $data); 58 59 /** 60 * @param string $sessionId 61 * 62 * @return bool 63 */ 64 abstract protected function doDestroy($sessionId); 65 66 /** 67 * @return bool 68 */ 69 public function validateId($sessionId) 70 { 71 $this->prefetchData = $this->read($sessionId); 72 $this->prefetchId = $sessionId; 73 74 if (\PHP_VERSION_ID < 70317 || (70400 <= \PHP_VERSION_ID && \PHP_VERSION_ID < 70405)) { 75 // work around https://bugs.php.net/79413 76 foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { 77 if (!isset($frame['class']) && isset($frame['function']) && \in_array($frame['function'], ['session_regenerate_id', 'session_create_id'], true)) { 78 return '' === $this->prefetchData; 79 } 80 } 81 } 82 83 return '' !== $this->prefetchData; 84 } 85 86 /** 87 * @return string 88 */ 89 public function read($sessionId) 90 { 91 if (null !== $this->prefetchId) { 92 $prefetchId = $this->prefetchId; 93 $prefetchData = $this->prefetchData; 94 $this->prefetchId = $this->prefetchData = null; 95 96 if ($prefetchId === $sessionId || '' === $prefetchData) { 97 $this->newSessionId = '' === $prefetchData ? $sessionId : null; 98 99 return $prefetchData; 100 } 101 } 102 103 $data = $this->doRead($sessionId); 104 $this->newSessionId = '' === $data ? $sessionId : null; 105 if (\PHP_VERSION_ID < 70000) { 106 $this->prefetchData = $data; 107 } 108 109 return $data; 110 } 111 112 /** 113 * @return bool 114 */ 115 public function write($sessionId, $data) 116 { 117 if (\PHP_VERSION_ID < 70000 && $this->prefetchData) { 118 $readData = $this->prefetchData; 119 $this->prefetchData = null; 120 121 if ($readData === $data) { 122 return $this->updateTimestamp($sessionId, $data); 123 } 124 } 125 if (null === $this->igbinaryEmptyData) { 126 // see https://github.com/igbinary/igbinary/issues/146 127 $this->igbinaryEmptyData = \function_exists('igbinary_serialize') ? igbinary_serialize([]) : ''; 128 } 129 if ('' === $data || $this->igbinaryEmptyData === $data) { 130 return $this->destroy($sessionId); 131 } 132 $this->newSessionId = null; 133 134 return $this->doWrite($sessionId, $data); 135 } 136 137 /** 138 * @return bool 139 */ 140 public function destroy($sessionId) 141 { 142 if (\PHP_VERSION_ID < 70000) { 143 $this->prefetchData = null; 144 } 145 if (!headers_sent() && filter_var(ini_get('session.use_cookies'), FILTER_VALIDATE_BOOLEAN)) { 146 if (!$this->sessionName) { 147 throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class)); 148 } 149 $cookie = SessionUtils::popSessionCookie($this->sessionName, $sessionId); 150 151 /* 152 * We send an invalidation Set-Cookie header (zero lifetime) 153 * when either the session was started or a cookie with 154 * the session name was sent by the client (in which case 155 * we know it's invalid as a valid session cookie would've 156 * started the session). 157 */ 158 if (null === $cookie || isset($_COOKIE[$this->sessionName])) { 159 if (\PHP_VERSION_ID < 70300) { 160 setcookie($this->sessionName, '', 0, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), filter_var(ini_get('session.cookie_secure'), FILTER_VALIDATE_BOOLEAN), filter_var(ini_get('session.cookie_httponly'), FILTER_VALIDATE_BOOLEAN)); 161 } else { 162 $params = session_get_cookie_params(); 163 unset($params['lifetime']); 164 setcookie($this->sessionName, '', $params); 165 } 166 } 167 } 168 169 return $this->newSessionId === $sessionId || $this->doDestroy($sessionId); 170 } 171} 172