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; 13 14use Symfony\Component\HttpFoundation\Session\SessionBagInterface; 15use Symfony\Component\HttpFoundation\Session\SessionUtils; 16use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; 17use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; 18use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; 19 20// Help opcache.preload discover always-needed symbols 21class_exists(MetadataBag::class); 22class_exists(StrictSessionHandler::class); 23class_exists(SessionHandlerProxy::class); 24 25/** 26 * This provides a base class for session attribute storage. 27 * 28 * @author Drak <drak@zikula.org> 29 */ 30class NativeSessionStorage implements SessionStorageInterface 31{ 32 /** 33 * @var SessionBagInterface[] 34 */ 35 protected $bags = []; 36 37 /** 38 * @var bool 39 */ 40 protected $started = false; 41 42 /** 43 * @var bool 44 */ 45 protected $closed = false; 46 47 /** 48 * @var AbstractProxy|\SessionHandlerInterface 49 */ 50 protected $saveHandler; 51 52 /** 53 * @var MetadataBag 54 */ 55 protected $metadataBag; 56 57 /** 58 * @var string|null 59 */ 60 private $emulateSameSite; 61 62 /** 63 * Depending on how you want the storage driver to behave you probably 64 * want to override this constructor entirely. 65 * 66 * List of options for $options array with their defaults. 67 * 68 * @see https://php.net/session.configuration for options 69 * but we omit 'session.' from the beginning of the keys for convenience. 70 * 71 * ("auto_start", is not supported as it tells PHP to start a session before 72 * PHP starts to execute user-land code. Setting during runtime has no effect). 73 * 74 * cache_limiter, "" (use "0" to prevent headers from being sent entirely). 75 * cache_expire, "0" 76 * cookie_domain, "" 77 * cookie_httponly, "" 78 * cookie_lifetime, "0" 79 * cookie_path, "/" 80 * cookie_secure, "" 81 * cookie_samesite, null 82 * gc_divisor, "100" 83 * gc_maxlifetime, "1440" 84 * gc_probability, "1" 85 * lazy_write, "1" 86 * name, "PHPSESSID" 87 * referer_check, "" 88 * serialize_handler, "php" 89 * use_strict_mode, "1" 90 * use_cookies, "1" 91 * use_only_cookies, "1" 92 * use_trans_sid, "0" 93 * sid_length, "32" 94 * sid_bits_per_character, "5" 95 * trans_sid_hosts, $_SERVER['HTTP_HOST'] 96 * trans_sid_tags, "a=href,area=href,frame=src,form=" 97 * 98 * @param AbstractProxy|\SessionHandlerInterface|null $handler 99 */ 100 public function __construct(array $options = [], $handler = null, MetadataBag $metaBag = null) 101 { 102 if (!\extension_loaded('session')) { 103 throw new \LogicException('PHP extension "session" is required.'); 104 } 105 106 $options += [ 107 'cache_limiter' => '', 108 'cache_expire' => 0, 109 'use_cookies' => 1, 110 'lazy_write' => 1, 111 'use_strict_mode' => 1, 112 ]; 113 114 session_register_shutdown(); 115 116 $this->setMetadataBag($metaBag); 117 $this->setOptions($options); 118 $this->setSaveHandler($handler); 119 } 120 121 /** 122 * Gets the save handler instance. 123 * 124 * @return AbstractProxy|\SessionHandlerInterface 125 */ 126 public function getSaveHandler() 127 { 128 return $this->saveHandler; 129 } 130 131 /** 132 * {@inheritdoc} 133 */ 134 public function start() 135 { 136 if ($this->started) { 137 return true; 138 } 139 140 if (\PHP_SESSION_ACTIVE === session_status()) { 141 throw new \RuntimeException('Failed to start the session: already started by PHP.'); 142 } 143 144 if (filter_var(ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN) && headers_sent($file, $line)) { 145 throw new \RuntimeException(sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line)); 146 } 147 148 // ok to try and start the session 149 if (!session_start()) { 150 throw new \RuntimeException('Failed to start the session.'); 151 } 152 153 if (null !== $this->emulateSameSite) { 154 $originalCookie = SessionUtils::popSessionCookie(session_name(), session_id()); 155 if (null !== $originalCookie) { 156 header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite), false); 157 } 158 } 159 160 $this->loadSession(); 161 162 return true; 163 } 164 165 /** 166 * {@inheritdoc} 167 */ 168 public function getId() 169 { 170 return $this->saveHandler->getId(); 171 } 172 173 /** 174 * {@inheritdoc} 175 */ 176 public function setId(string $id) 177 { 178 $this->saveHandler->setId($id); 179 } 180 181 /** 182 * {@inheritdoc} 183 */ 184 public function getName() 185 { 186 return $this->saveHandler->getName(); 187 } 188 189 /** 190 * {@inheritdoc} 191 */ 192 public function setName(string $name) 193 { 194 $this->saveHandler->setName($name); 195 } 196 197 /** 198 * {@inheritdoc} 199 */ 200 public function regenerate(bool $destroy = false, int $lifetime = null) 201 { 202 // Cannot regenerate the session ID for non-active sessions. 203 if (\PHP_SESSION_ACTIVE !== session_status()) { 204 return false; 205 } 206 207 if (headers_sent()) { 208 return false; 209 } 210 211 if (null !== $lifetime && $lifetime != ini_get('session.cookie_lifetime')) { 212 $this->save(); 213 ini_set('session.cookie_lifetime', $lifetime); 214 $this->start(); 215 } 216 217 if ($destroy) { 218 $this->metadataBag->stampNew(); 219 } 220 221 $isRegenerated = session_regenerate_id($destroy); 222 223 if (null !== $this->emulateSameSite) { 224 $originalCookie = SessionUtils::popSessionCookie(session_name(), session_id()); 225 if (null !== $originalCookie) { 226 header(sprintf('%s; SameSite=%s', $originalCookie, $this->emulateSameSite), false); 227 } 228 } 229 230 return $isRegenerated; 231 } 232 233 /** 234 * {@inheritdoc} 235 */ 236 public function save() 237 { 238 // Store a copy so we can restore the bags in case the session was not left empty 239 $session = $_SESSION; 240 241 foreach ($this->bags as $bag) { 242 if (empty($_SESSION[$key = $bag->getStorageKey()])) { 243 unset($_SESSION[$key]); 244 } 245 } 246 if ([$key = $this->metadataBag->getStorageKey()] === array_keys($_SESSION)) { 247 unset($_SESSION[$key]); 248 } 249 250 // Register error handler to add information about the current save handler 251 $previousHandler = set_error_handler(function ($type, $msg, $file, $line) use (&$previousHandler) { 252 if (\E_WARNING === $type && str_starts_with($msg, 'session_write_close():')) { 253 $handler = $this->saveHandler instanceof SessionHandlerProxy ? $this->saveHandler->getHandler() : $this->saveHandler; 254 $msg = sprintf('session_write_close(): Failed to write session data with "%s" handler', \get_class($handler)); 255 } 256 257 return $previousHandler ? $previousHandler($type, $msg, $file, $line) : false; 258 }); 259 260 try { 261 session_write_close(); 262 } finally { 263 restore_error_handler(); 264 265 // Restore only if not empty 266 if ($_SESSION) { 267 $_SESSION = $session; 268 } 269 } 270 271 $this->closed = true; 272 $this->started = false; 273 } 274 275 /** 276 * {@inheritdoc} 277 */ 278 public function clear() 279 { 280 // clear out the bags 281 foreach ($this->bags as $bag) { 282 $bag->clear(); 283 } 284 285 // clear out the session 286 $_SESSION = []; 287 288 // reconnect the bags to the session 289 $this->loadSession(); 290 } 291 292 /** 293 * {@inheritdoc} 294 */ 295 public function registerBag(SessionBagInterface $bag) 296 { 297 if ($this->started) { 298 throw new \LogicException('Cannot register a bag when the session is already started.'); 299 } 300 301 $this->bags[$bag->getName()] = $bag; 302 } 303 304 /** 305 * {@inheritdoc} 306 */ 307 public function getBag(string $name) 308 { 309 if (!isset($this->bags[$name])) { 310 throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name)); 311 } 312 313 if (!$this->started && $this->saveHandler->isActive()) { 314 $this->loadSession(); 315 } elseif (!$this->started) { 316 $this->start(); 317 } 318 319 return $this->bags[$name]; 320 } 321 322 public function setMetadataBag(MetadataBag $metaBag = null) 323 { 324 if (null === $metaBag) { 325 $metaBag = new MetadataBag(); 326 } 327 328 $this->metadataBag = $metaBag; 329 } 330 331 /** 332 * Gets the MetadataBag. 333 * 334 * @return MetadataBag 335 */ 336 public function getMetadataBag() 337 { 338 return $this->metadataBag; 339 } 340 341 /** 342 * {@inheritdoc} 343 */ 344 public function isStarted() 345 { 346 return $this->started; 347 } 348 349 /** 350 * Sets session.* ini variables. 351 * 352 * For convenience we omit 'session.' from the beginning of the keys. 353 * Explicitly ignores other ini keys. 354 * 355 * @param array $options Session ini directives [key => value] 356 * 357 * @see https://php.net/session.configuration 358 */ 359 public function setOptions(array $options) 360 { 361 if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { 362 return; 363 } 364 365 $validOptions = array_flip([ 366 'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly', 367 'cookie_lifetime', 'cookie_path', 'cookie_secure', 'cookie_samesite', 368 'gc_divisor', 'gc_maxlifetime', 'gc_probability', 369 'lazy_write', 'name', 'referer_check', 370 'serialize_handler', 'use_strict_mode', 'use_cookies', 371 'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled', 372 'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name', 373 'upload_progress.freq', 'upload_progress.min_freq', 'url_rewriter.tags', 374 'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags', 375 ]); 376 377 foreach ($options as $key => $value) { 378 if (isset($validOptions[$key])) { 379 if (str_starts_with($key, 'upload_progress.')) { 380 trigger_deprecation('symfony/http-foundation', '5.4', 'Support for the "%s" session option is deprecated. The settings prefixed with "session.upload_progress." can not be changed at runtime.', $key); 381 continue; 382 } 383 if ('url_rewriter.tags' === $key) { 384 trigger_deprecation('symfony/http-foundation', '5.4', 'Support for the "%s" session option is deprecated. Use "trans_sid_tags" instead.', $key); 385 } 386 if ('cookie_samesite' === $key && \PHP_VERSION_ID < 70300) { 387 // PHP < 7.3 does not support same_site cookies. We will emulate it in 388 // the start() method instead. 389 $this->emulateSameSite = $value; 390 continue; 391 } 392 if ('cookie_secure' === $key && 'auto' === $value) { 393 continue; 394 } 395 ini_set('url_rewriter.tags' !== $key ? 'session.'.$key : $key, $value); 396 } 397 } 398 } 399 400 /** 401 * Registers session save handler as a PHP session handler. 402 * 403 * To use internal PHP session save handlers, override this method using ini_set with 404 * session.save_handler and session.save_path e.g. 405 * 406 * ini_set('session.save_handler', 'files'); 407 * ini_set('session.save_path', '/tmp'); 408 * 409 * or pass in a \SessionHandler instance which configures session.save_handler in the 410 * constructor, for a template see NativeFileSessionHandler. 411 * 412 * @see https://php.net/session-set-save-handler 413 * @see https://php.net/sessionhandlerinterface 414 * @see https://php.net/sessionhandler 415 * 416 * @param AbstractProxy|\SessionHandlerInterface|null $saveHandler 417 * 418 * @throws \InvalidArgumentException 419 */ 420 public function setSaveHandler($saveHandler = null) 421 { 422 if (!$saveHandler instanceof AbstractProxy && 423 !$saveHandler instanceof \SessionHandlerInterface && 424 null !== $saveHandler) { 425 throw new \InvalidArgumentException('Must be instance of AbstractProxy; implement \SessionHandlerInterface; or be null.'); 426 } 427 428 // Wrap $saveHandler in proxy and prevent double wrapping of proxy 429 if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) { 430 $saveHandler = new SessionHandlerProxy($saveHandler); 431 } elseif (!$saveHandler instanceof AbstractProxy) { 432 $saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler())); 433 } 434 $this->saveHandler = $saveHandler; 435 436 if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { 437 return; 438 } 439 440 if ($this->saveHandler instanceof SessionHandlerProxy) { 441 session_set_save_handler($this->saveHandler, false); 442 } 443 } 444 445 /** 446 * Load the session with attributes. 447 * 448 * After starting the session, PHP retrieves the session from whatever handlers 449 * are set to (either PHP's internal, or a custom save handler set with session_set_save_handler()). 450 * PHP takes the return value from the read() handler, unserializes it 451 * and populates $_SESSION with the result automatically. 452 */ 453 protected function loadSession(array &$session = null) 454 { 455 if (null === $session) { 456 $session = &$_SESSION; 457 } 458 459 $bags = array_merge($this->bags, [$this->metadataBag]); 460 461 foreach ($bags as $bag) { 462 $key = $bag->getStorageKey(); 463 $session[$key] = isset($session[$key]) && \is_array($session[$key]) ? $session[$key] : []; 464 $bag->initialize($session[$key]); 465 } 466 467 $this->started = true; 468 $this->closed = false; 469 } 470} 471