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 #[\ReturnTypeWillChange] 35 public function open($savePath, $sessionName) 36 { 37 $this->sessionName = $sessionName; 38 if (!headers_sent() && !ini_get('session.cache_limiter') && '0' !== ini_get('session.cache_limiter')) { 39 header(sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) ini_get('session.cache_expire'))); 40 } 41 42 return true; 43 } 44 45 /** 46 * @return string 47 */ 48 abstract protected function doRead(string $sessionId); 49 50 /** 51 * @return bool 52 */ 53 abstract protected function doWrite(string $sessionId, string $data); 54 55 /** 56 * @return bool 57 */ 58 abstract protected function doDestroy(string $sessionId); 59 60 /** 61 * @return bool 62 */ 63 #[\ReturnTypeWillChange] 64 public function validateId($sessionId) 65 { 66 $this->prefetchData = $this->read($sessionId); 67 $this->prefetchId = $sessionId; 68 69 if (\PHP_VERSION_ID < 70317 || (70400 <= \PHP_VERSION_ID && \PHP_VERSION_ID < 70405)) { 70 // work around https://bugs.php.net/79413 71 foreach (debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { 72 if (!isset($frame['class']) && isset($frame['function']) && \in_array($frame['function'], ['session_regenerate_id', 'session_create_id'], true)) { 73 return '' === $this->prefetchData; 74 } 75 } 76 } 77 78 return '' !== $this->prefetchData; 79 } 80 81 /** 82 * @return string 83 */ 84 #[\ReturnTypeWillChange] 85 public function read($sessionId) 86 { 87 if (null !== $this->prefetchId) { 88 $prefetchId = $this->prefetchId; 89 $prefetchData = $this->prefetchData; 90 $this->prefetchId = $this->prefetchData = null; 91 92 if ($prefetchId === $sessionId || '' === $prefetchData) { 93 $this->newSessionId = '' === $prefetchData ? $sessionId : null; 94 95 return $prefetchData; 96 } 97 } 98 99 $data = $this->doRead($sessionId); 100 $this->newSessionId = '' === $data ? $sessionId : null; 101 102 return $data; 103 } 104 105 /** 106 * @return bool 107 */ 108 #[\ReturnTypeWillChange] 109 public function write($sessionId, $data) 110 { 111 if (null === $this->igbinaryEmptyData) { 112 // see https://github.com/igbinary/igbinary/issues/146 113 $this->igbinaryEmptyData = \function_exists('igbinary_serialize') ? igbinary_serialize([]) : ''; 114 } 115 if ('' === $data || $this->igbinaryEmptyData === $data) { 116 return $this->destroy($sessionId); 117 } 118 $this->newSessionId = null; 119 120 return $this->doWrite($sessionId, $data); 121 } 122 123 /** 124 * @return bool 125 */ 126 #[\ReturnTypeWillChange] 127 public function destroy($sessionId) 128 { 129 if (!headers_sent() && filter_var(ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN)) { 130 if (!$this->sessionName) { 131 throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class)); 132 } 133 $cookie = SessionUtils::popSessionCookie($this->sessionName, $sessionId); 134 135 /* 136 * We send an invalidation Set-Cookie header (zero lifetime) 137 * when either the session was started or a cookie with 138 * the session name was sent by the client (in which case 139 * we know it's invalid as a valid session cookie would've 140 * started the session). 141 */ 142 if (null === $cookie || isset($_COOKIE[$this->sessionName])) { 143 if (\PHP_VERSION_ID < 70300) { 144 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)); 145 } else { 146 $params = session_get_cookie_params(); 147 unset($params['lifetime']); 148 setcookie($this->sessionName, '', $params); 149 } 150 } 151 } 152 153 return $this->newSessionId === $sessionId || $this->doDestroy($sessionId); 154 } 155} 156