123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562 |
- <?php
- /**
- * @package Grav\Framework\Session
- *
- * @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
- * @license MIT License; see LICENSE file for details.
- */
- namespace Grav\Framework\Session;
- use ArrayIterator;
- use Exception;
- use Throwable;
- use Grav\Common\Debugger;
- use Grav\Common\Grav;
- use Grav\Common\User\Interfaces\UserInterface;
- use Grav\Framework\Session\Exceptions\SessionException;
- use RuntimeException;
- use function is_array;
- use function is_bool;
- use function is_string;
- /**
- * Class Session
- * @package Grav\Framework\Session
- */
- class Session implements SessionInterface
- {
- /** @var array */
- protected $options = [];
- /** @var bool */
- protected $started = false;
- /** @var Session */
- protected static $instance;
- /**
- * @inheritdoc
- */
- public static function getInstance()
- {
- if (null === self::$instance) {
- throw new RuntimeException("Session hasn't been initialized.", 500);
- }
- return self::$instance;
- }
- /**
- * Session constructor.
- *
- * @param array $options
- */
- public function __construct(array $options = [])
- {
- // Session is a singleton.
- if (\PHP_SAPI === 'cli') {
- self::$instance = $this;
- return;
- }
- if (null !== self::$instance) {
- throw new RuntimeException('Session has already been initialized.', 500);
- }
- // Destroy any existing sessions started with session.auto_start
- if ($this->isSessionStarted()) {
- session_unset();
- session_destroy();
- }
- // Set default options.
- $options += [
- 'cache_limiter' => 'nocache',
- 'use_trans_sid' => 0,
- 'use_cookies' => 1,
- 'lazy_write' => 1,
- 'use_strict_mode' => 1
- ];
- $this->setOptions($options);
- session_register_shutdown();
- self::$instance = $this;
- }
- /**
- * @inheritdoc
- */
- public function getId()
- {
- return session_id() ?: null;
- }
- /**
- * @inheritdoc
- */
- public function setId($id)
- {
- session_id($id);
- return $this;
- }
- /**
- * @inheritdoc
- */
- public function getName()
- {
- return session_name() ?: null;
- }
- /**
- * @inheritdoc
- */
- public function setName($name)
- {
- session_name($name);
- return $this;
- }
- /**
- * @inheritdoc
- */
- public function setOptions(array $options)
- {
- if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) {
- return;
- }
- $allowedOptions = [
- 'save_path' => true,
- 'name' => true,
- 'save_handler' => true,
- 'gc_probability' => true,
- 'gc_divisor' => true,
- 'gc_maxlifetime' => true,
- 'serialize_handler' => true,
- 'cookie_lifetime' => true,
- 'cookie_path' => true,
- 'cookie_domain' => true,
- 'cookie_secure' => true,
- 'cookie_httponly' => true,
- 'use_strict_mode' => true,
- 'use_cookies' => true,
- 'use_only_cookies' => true,
- 'cookie_samesite' => true,
- 'referer_check' => true,
- 'cache_limiter' => true,
- 'cache_expire' => true,
- 'use_trans_sid' => true,
- 'trans_sid_tags' => true,
- 'trans_sid_hosts' => true,
- 'sid_length' => true,
- 'sid_bits_per_character' => true,
- 'upload_progress.enabled' => true,
- 'upload_progress.cleanup' => true,
- 'upload_progress.prefix' => true,
- 'upload_progress.name' => true,
- 'upload_progress.freq' => true,
- 'upload_progress.min-freq' => true,
- 'lazy_write' => true
- ];
- foreach ($options as $key => $value) {
- if (is_array($value)) {
- // Allow nested options.
- foreach ($value as $key2 => $value2) {
- $ckey = "{$key}.{$key2}";
- if (isset($value2, $allowedOptions[$ckey])) {
- $this->setOption($ckey, $value2);
- }
- }
- } elseif (isset($value, $allowedOptions[$key])) {
- $this->setOption($key, $value);
- }
- }
- }
- /**
- * @inheritdoc
- */
- public function start($readonly = false)
- {
- if (\PHP_SAPI === 'cli') {
- return $this;
- }
- $sessionName = $this->getName();
- if (null === $sessionName) {
- return $this;
- }
- $sessionExists = isset($_COOKIE[$sessionName]);
- // Protection against invalid session cookie names throwing exception: http://php.net/manual/en/function.session-id.php#116836
- if ($sessionExists && !preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $_COOKIE[$sessionName])) {
- unset($_COOKIE[$sessionName]);
- $sessionExists = false;
- }
- $options = $this->options;
- if ($readonly) {
- $options['read_and_close'] = '1';
- }
- try {
- $success = @session_start($options);
- if (!$success) {
- $last = error_get_last();
- $error = $last ? $last['message'] : 'Unknown error';
- throw new RuntimeException($error);
- }
- // Handle changing session id.
- if ($this->__isset('session_destroyed')) {
- $newId = $this->__get('session_new_id');
- if (!$newId || $this->__get('session_destroyed') < time() - 300) {
- // Should not happen usually. This could be attack or due to unstable network. Destroy this session.
- $this->invalidate();
- throw new RuntimeException('Obsolete session access.', 500);
- }
- // Not fully expired yet. Could be lost cookie by unstable network. Start session with new session id.
- session_write_close();
- // Start session with new session id.
- $useStrictMode = $options['use_strict_mode'] ?? 0;
- if ($useStrictMode) {
- ini_set('session.use_strict_mode', '0');
- }
- session_id($newId);
- if ($useStrictMode) {
- ini_set('session.use_strict_mode', '1');
- }
- $success = @session_start($options);
- if (!$success) {
- $last = error_get_last();
- $error = $last ? $last['message'] : 'Unknown error';
- throw new RuntimeException($error);
- }
- }
- } catch (Exception $e) {
- throw new SessionException('Failed to start session: ' . $e->getMessage(), 500);
- }
- $this->started = true;
- $this->onSessionStart();
- try {
- $user = $this->__get('user');
- if ($user && (!$user instanceof UserInterface || (method_exists($user, 'isValid') && !$user->isValid()))) {
- throw new RuntimeException('Bad user');
- }
- } catch (Throwable $e) {
- $this->invalidate();
- throw new SessionException('Invalid User object, session destroyed.', 500);
- }
- // Extend the lifetime of the session.
- if ($sessionExists) {
- $this->setCookie();
- }
- return $this;
- }
- /**
- * Regenerate session id but keep the current session information.
- *
- * Session id must be regenerated on login, logout or after long time has been passed.
- *
- * @return $this
- * @since 1.7
- */
- public function regenerateId()
- {
- if (!$this->isSessionStarted()) {
- return $this;
- }
- // TODO: session_create_id() segfaults in PHP 7.3 (PHP bug #73461), remove phpstan rule when removing this one.
- if (PHP_VERSION_ID < 70400) {
- $newId = 0;
- } else {
- // Session id creation may fail with some session storages.
- $newId = @session_create_id() ?: 0;
- }
- // Set destroyed timestamp for the old session as well as pointer to the new id.
- $this->__set('session_destroyed', time());
- $this->__set('session_new_id', $newId);
- // Keep the old session alive to avoid lost sessions by unstable network.
- if (!$newId) {
- /** @var Debugger $debugger */
- $debugger = Grav::instance()['debugger'];
- $debugger->addMessage('Session fixation lost session detection is turned of due to server limitations.', 'warning');
- session_regenerate_id(false);
- } else {
- session_write_close();
- // Start session with new session id.
- $useStrictMode = $this->options['use_strict_mode'] ?? 0;
- if ($useStrictMode) {
- ini_set('session.use_strict_mode', '0');
- }
- session_id($newId);
- if ($useStrictMode) {
- ini_set('session.use_strict_mode', '1');
- }
- $this->removeCookie();
- $this->onBeforeSessionStart();
- $success = @session_start($this->options);
- if (!$success) {
- $last = error_get_last();
- $error = $last ? $last['message'] : 'Unknown error';
- throw new RuntimeException($error);
- }
- $this->onSessionStart();
- }
- // New session does not have these.
- $this->__unset('session_destroyed');
- $this->__unset('session_new_id');
- return $this;
- }
- /**
- * @inheritdoc
- */
- public function invalidate()
- {
- $name = $this->getName();
- if (null !== $name) {
- $this->removeCookie();
- setcookie(
- $name,
- '',
- $this->getCookieOptions(-42000)
- );
- }
- if ($this->isSessionStarted()) {
- session_unset();
- session_destroy();
- }
- $this->started = false;
- return $this;
- }
- /**
- * @inheritdoc
- */
- public function close()
- {
- if ($this->started) {
- session_write_close();
- }
- $this->started = false;
- return $this;
- }
- /**
- * @inheritdoc
- */
- public function clear()
- {
- session_unset();
- return $this;
- }
- /**
- * @inheritdoc
- */
- public function getAll()
- {
- return $_SESSION;
- }
- /**
- * @inheritdoc
- */
- #[\ReturnTypeWillChange]
- public function getIterator()
- {
- return new ArrayIterator($_SESSION);
- }
- /**
- * @inheritdoc
- */
- public function isStarted()
- {
- return $this->started;
- }
- /**
- * @inheritdoc
- */
- #[\ReturnTypeWillChange]
- public function __isset($name)
- {
- return isset($_SESSION[$name]);
- }
- /**
- * @inheritdoc
- */
- #[\ReturnTypeWillChange]
- public function __get($name)
- {
- return $_SESSION[$name] ?? null;
- }
- /**
- * @inheritdoc
- */
- #[\ReturnTypeWillChange]
- public function __set($name, $value)
- {
- $_SESSION[$name] = $value;
- }
- /**
- * @inheritdoc
- */
- #[\ReturnTypeWillChange]
- public function __unset($name)
- {
- unset($_SESSION[$name]);
- }
- /**
- * http://php.net/manual/en/function.session-status.php#113468
- * Check if session is started nicely.
- * @return bool
- */
- protected function isSessionStarted()
- {
- return \PHP_SAPI !== 'cli' ? \PHP_SESSION_ACTIVE === session_status() : false;
- }
- protected function onBeforeSessionStart(): void
- {
- }
- protected function onSessionStart(): void
- {
- }
- /**
- * Store something in cookie temporarily.
- *
- * @param int|null $lifetime
- * @return array
- */
- public function getCookieOptions(int $lifetime = null): array
- {
- $params = session_get_cookie_params();
- return [
- 'expires' => time() + ($lifetime ?? $params['lifetime']),
- 'path' => $params['path'],
- 'domain' => $params['domain'],
- 'secure' => $params['secure'],
- 'httponly' => $params['httponly'],
- 'samesite' => $params['samesite']
- ];
- }
- /**
- * @return void
- */
- protected function setCookie(): void
- {
- $this->removeCookie();
- $sessionName = $this->getName();
- $sessionId = $this->getId();
- if (null === $sessionName || null === $sessionId) {
- return;
- }
- setcookie(
- $sessionName,
- $sessionId,
- $this->getCookieOptions()
- );
- }
- protected function removeCookie(): void
- {
- $search = " {$this->getName()}=";
- $cookies = [];
- $found = false;
- foreach (headers_list() as $header) {
- // Identify cookie headers
- if (strpos($header, 'Set-Cookie:') === 0) {
- // Add all but session cookie(s).
- if (!str_contains($header, $search)) {
- $cookies[] = $header;
- } else {
- $found = true;
- }
- }
- }
- // Nothing to do.
- if (false === $found) {
- return;
- }
- // Remove all cookies and put back all but session cookie.
- header_remove('Set-Cookie');
- foreach($cookies as $cookie) {
- header($cookie, false);
- }
- }
- /**
- * @param string $key
- * @param mixed $value
- * @return void
- */
- protected function setOption($key, $value)
- {
- if (!is_string($value)) {
- if (is_bool($value)) {
- $value = $value ? '1' : '0';
- } else {
- $value = (string)$value;
- }
- }
- $this->options[$key] = $value;
- ini_set("session.{$key}", $value);
- }
- }
|