* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Framework\Session; use Framework\Session\Debug\SessionCollector; use JetBrains\PhpStorm\Pure; use LogicException; use RuntimeException; /** * Class Session. * * @package session */ class Session { /** * @var array */ protected array $options = []; protected SaveHandler $saveHandler; protected SessionCollector $debugCollector; /** * Session constructor. * * @param array $options * @param SaveHandler|null $handler */ public function __construct(array $options = [], ?SaveHandler $handler = null) { $this->setOptions($options); if ($handler) { $this->saveHandler = $handler; \session_set_save_handler($handler); } } public function __destruct() { $this->stop(); } public function __get(string $key) : mixed { return $this->get($key); } public function __set(string $key, mixed $value) : void { $this->set($key, $value); } public function __isset(string $key) : bool { return $this->has($key); } public function __unset(string $key) : void { $this->remove($key); } /** * @see http://php.net/manual/en/session.security.ini.php * * @param array $custom */ protected function setOptions(array $custom) : void { $serializer = \ini_get('session.serialize_handler'); $serializer = $serializer === 'php' ? 'php_serialize' : $serializer; $secure = (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https') || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on'); $default = [ 'name' => 'session_id', 'serialize_handler' => $serializer, 'cookie_domain' => '', 'cookie_httponly' => 1, 'cookie_lifetime' => 7200, 'cookie_path' => '/', 'cookie_samesite' => 'Strict', 'cookie_secure' => $secure, 'referer_check' => '', 'use_cookies' => 1, 'use_only_cookies' => 1, 'use_strict_mode' => 1, 'use_trans_sid' => 0, // used to auto-regenerate the session id: 'auto_regenerate_maxlifetime' => 0, 'auto_regenerate_destroy' => true, 'set_cookie_permanent' => false, ]; if (\PHP_VERSION_ID < 80400) { $default['sid_bits_per_character'] = 6; $default['sid_length'] = 48; } $this->options = $custom ? \array_replace($default, $custom) : $default; } /** * @param array $custom * * @return array */ protected function getOptions(array $custom = []) : array { $options = $custom ? \array_replace($this->options, $custom) : $this->options; unset( $options['auto_regenerate_maxlifetime'], $options['auto_regenerate_destroy'], $options['set_cookie_permanent'], ); return $options; } /** * @param array $customOptions * * @throws LogicException if session was already active * @throws RuntimeException if session could not be started * * @return bool */ public function start(array $customOptions = []) : bool { if ($this->isActive()) { throw new LogicException('Session was already active'); } if (!@\session_start($this->getOptions($customOptions))) { $message = ''; if (\error_get_last()) { $message = ': ' . \error_get_last()['message']; } throw new RuntimeException( 'Session could not be started' . $message ); } $time = \time(); $this->setPermanentCookie($time); $this->autoRegenerate($time); $this->clearTemp($time); $this->clearFlash(); return true; } /** * Make sure the session is active. * * If it is not active, it will start it. * * @throws RuntimeException if session could not be started * * @return bool */ public function activate() : bool { if ($this->isActive()) { return true; } return $this->start(); } /** * @param int $time * * @see https://www.php.net/manual/en/function.session-set-cookie-params.php#100657 * @see https://stackoverflow.com/a/34252812/6027968 */ protected function setPermanentCookie(int $time) : void { $setCookie = (bool) $this->options['set_cookie_permanent']; if ($setCookie === false) { return; } $params = \session_get_cookie_params(); \setcookie( \session_name(), // @phpstan-ignore-line \session_id(), // @phpstan-ignore-line [ // @phpstan-ignore-line 'expires' => $time + $this->options['cookie_lifetime'], 'path' => $params['path'], 'domain' => $params['domain'], 'secure' => $params['secure'], 'httponly' => $params['httponly'], 'samesite' => $params['samesite'], ] ); } /** * Auto regenerate the session id. * * @param int $time * * @see https://owasp.org/www-community/attacks/Session_fixation */ protected function autoRegenerate(int $time) : void { $maxlifetime = (int) $this->options['auto_regenerate_maxlifetime']; $isActive = $maxlifetime > 0; if (($isActive && empty($_SESSION['$']['regenerated_at'])) || ($isActive && $_SESSION['$']['regenerated_at'] < ($time - $maxlifetime)) ) { $this->regenerateId((bool) $this->options['auto_regenerate_destroy']); } } /** * Clears the Flash Data. */ protected function clearFlash() : void { unset($_SESSION['$']['flash']['old']); if (isset($_SESSION['$']['flash']['new'])) { foreach ($_SESSION['$']['flash']['new'] as $key => $value) { $_SESSION['$']['flash']['old'][$key] = $value; } } unset($_SESSION['$']['flash']['new']); if (empty($_SESSION['$']['flash'])) { unset($_SESSION['$']['flash']); } } /** * Clears the Temp Data. * * @param int $time The max time to temp data survive */ protected function clearTemp(int $time) : void { if (isset($_SESSION['$']['temp'])) { foreach ($_SESSION['$']['temp'] as $key => $value) { if ($value['ttl'] < $time) { unset($_SESSION['$']['temp'][$key]); } } } if (empty($_SESSION['$']['temp'])) { unset($_SESSION['$']['temp']); } } /** * Tells if sessions are enabled, and one exists. * * @return bool */ public function isActive() : bool { return \session_status() === \PHP_SESSION_ACTIVE; } /** * Destroys all data registered to a session. * * @return bool true on success or false on failure */ public function destroy() : bool { if ($this->isActive()) { $destroyed = \session_destroy(); } unset($_SESSION); return $destroyed ?? true; } /** * Sets a Cookie with the session name to be destroyed in the user-agent. * * @throws RuntimeException If it could not get the session name * * @return bool True if the Set-Cookie header was set to invalidate the * session cookie, false if output exists */ public function destroyCookie() : bool { $name = \session_name(); if ($name === false) { throw new RuntimeException('Could not get the session name'); } $params = \session_get_cookie_params(); // @phpstan-ignore-next-line return \setcookie($name, '', [ 'expires' => 0, 'path' => $params['path'], 'domain' => $params['domain'], 'secure' => $params['secure'], 'httponly' => $params['httponly'], 'samesite' => $params['samesite'], ]); } /** * Write session data and end session. * * @return bool returns true on success or false on failure */ public function stop() : bool { if ($this->isActive()) { $closed = \session_write_close(); } return $closed ?? true; } /** * Discard session data changes and end session. * * @return bool returns true on success or false on failure */ public function abort() : bool { if ($this->isActive()) { $aborted = \session_abort(); } return $aborted ?? true; } /** * Tells if the session has an item. * * @param string $key The item key name * * @return bool True if it has, otherwise false */ #[Pure] public function has(string $key) : bool { return isset($_SESSION[$key]); } /** * Gets one session item. * * @param string $key The item key name * * @return mixed The item value or null if no set */ #[Pure] public function get(string $key) : mixed { return $_SESSION[$key] ?? null; } /** * Get all session items. * * @return array The value of the $_SESSION global */ #[Pure] public function getAll() : array { return $_SESSION; } /** * Get multiple session items. * * @param array $keys An array of key item names * * @return array An associative array with items keys and * values. Item not set will return as null. */ #[Pure] public function getMulti(array $keys) : array { $items = []; foreach ($keys as $key) { $items[$key] = $this->get($key); } return $items; } /** * Set a session item. * * @param string $key The item key name * @param mixed $value The item value * * @rerun static */ public function set(string $key, mixed $value) : static { $_SESSION[$key] = $value; return $this; } /** * Set multiple session items. * * @param array $items An associative array of items keys and * values * * @rerun static */ public function setMulti(array $items) : static { foreach ($items as $key => $value) { $this->set($key, $value); } return $this; } /** * Remove (unset) a session item. * * @param string $key The item key name * * @rerun static */ public function remove(string $key) : static { unset($_SESSION[$key]); return $this; } /** * Remove (unset) multiple session items. * * @param array $keys A list of items keys names * * @rerun static */ public function removeMulti(array $keys) : static { foreach ($keys as $key) { $this->remove($key); } return $this; } /** * Remove (unset) all session items. * * @rerun static */ public function removeAll() : static { @\session_unset(); $_SESSION = []; return $this; } /** * Update the current session id with a newly generated one. * * @param bool $deleteOldSession Whether to delete the old associated session item or not * * @return bool */ public function regenerateId(bool $deleteOldSession = false) : bool { $regenerated = \session_regenerate_id($deleteOldSession); if ($regenerated) { $_SESSION['$']['regenerated_at'] = \time(); } return $regenerated; } /** * Re-initialize session array with original values. * * @return bool true if the session was successfully reinitialized or false on failure */ public function reset() : bool { return \session_reset(); } /** * Get a Flash Data item. * * @param string $key The Flash item key name * * @return mixed The item value or null if not exists */ #[Pure] public function getFlash(string $key) : mixed { return $_SESSION['$']['flash']['new'][$key] ?? $_SESSION['$']['flash']['old'][$key] ?? null; } /** * Set a Flash Data item, available only in the next time the session is started. * * @param string $key The Flash Data item key name * @param mixed $value The item value * * @rerun static */ public function setFlash(string $key, mixed $value) : static { $_SESSION['$']['flash']['new'][$key] = $value; return $this; } /** * Remove a Flash Data item. * * @param string $key The item key name * * @rerun static */ public function removeFlash(string $key) : static { unset( $_SESSION['$']['flash']['old'][$key], $_SESSION['$']['flash']['new'][$key] ); return $this; } /** * Get a Temp Data item. * * @param string $key The item key name * * @return mixed The item value or null if it is expired or not set */ public function getTemp(string $key) : mixed { if (isset($_SESSION['$']['temp'][$key])) { if ($_SESSION['$']['temp'][$key]['ttl'] > \time()) { return $_SESSION['$']['temp'][$key]['data']; } unset($_SESSION['$']['temp'][$key]); } return null; } /** * Set a Temp Data item. * * @param string $key The item key name * @param mixed $value The item value * @param int $ttl The Time-To-Live of the item, in seconds * * @rerun static */ public function setTemp(string $key, mixed $value, int $ttl = 60) : static { $_SESSION['$']['temp'][$key] = [ 'ttl' => \time() + $ttl, 'data' => $value, ]; return $this; } /** * Remove (unset) a Temp Data item. * * @param string $key The item key name * * @rerun static */ public function removeTemp(string $key) : static { unset($_SESSION['$']['temp'][$key]); return $this; } /** * Get/Set the session id. * * @param string|null $newId [optional] The new session id * * @throws LogicException when trying to set a new id and the session is active * * @return false|string The old session id or false on failure. Note: If a * $newId is set, it is accepted but not validated. When session_start is * called, the id is only used if it is valid */ public function id(?string $newId = null) : false | string { if ($newId !== null && $this->isActive()) { throw new LogicException( 'Session ID cannot be changed when a session is active' ); } return \session_id($newId); } /** * Perform session data garbage collection. * * If return false, use {@see error_get_last()} to get error details. * * @return false|int Returns the number of deleted session data for success, * false for failure */ public function gc() : false | int { return @\session_gc(); } public function setDebugCollector(SessionCollector $collector) : static { $this->debugCollector = $collector; $this->debugCollector->setSession($this)->setOptions($this->options); if (isset($this->saveHandler)) { $this->debugCollector->setSaveHandler($this->saveHandler); } return $this; } }