* @link https://getkirby.com * @copyright Bastian Allgeier * @license https://opensource.org/licenses/MIT */ class Sessions { protected SessionStore $store; protected string $mode; protected string $cookieName; protected array $cache = []; /** * Creates a new Sessions instance * * @param \Kirby\Session\SessionStore|string $store SessionStore object or a path to the storage directory (uses the FileSessionStore) * @param array $options Optional additional options: * - `mode`: Default token transmission mode (cookie, header or manual); defaults to `cookie` * - `cookieName`: Name to use for the session cookie; defaults to `kirby_session` * - `gcInterval`: How often should the garbage collector be run?; integer or `false` for never; defaults to `100` */ public function __construct(SessionStore|string $store, array $options = []) { $this->store = match (is_string($store)) { true => new FileSessionStore($store), default => $store }; $this->mode = $options['mode'] ?? 'cookie'; $this->cookieName = $options['cookieName'] ?? 'kirby_session'; $gcInterval = $options['gcInterval'] ?? 100; // validate options if (in_array($this->mode, ['cookie', 'header', 'manual']) === false) { throw new InvalidArgumentException([ 'data' => [ 'method' => 'Sessions::__construct', 'argument' => '$options[\'mode\']' ], 'translate' => false ]); } // trigger automatic garbage collection with the given probability if (is_int($gcInterval) === true && $gcInterval > 0) { // convert the interval into a probability between 0 and 1 $gcProbability = 1 / $gcInterval; // generate a random number $random = mt_rand(1, 10000); // $random will be below or equal $gcProbability * 10000 with a probability of $gcProbability if ($random <= $gcProbability * 10000) { $this->collectGarbage(); } } elseif ($gcInterval !== false) { throw new InvalidArgumentException([ 'data' => [ 'method' => 'Sessions::__construct', 'argument' => '$options[\'gcInterval\']' ], 'translate' => false ]); } } /** * Creates a new empty session * * @param array $options Optional additional options: * - `mode`: Token transmission mode (cookie or manual); defaults to default mode of the Sessions instance * - `startTime`: Time the session starts being valid (date string or timestamp); defaults to `now` * - `expiryTime`: Time the session expires (date string or timestamp); defaults to `+ 2 hours` * - `timeout`: Activity timeout in seconds (integer or false for none); defaults to `1800` (half an hour) * - `renewable`: Should it be possible to extend the expiry date?; defaults to `true` */ public function create(array $options = []): Session { // fall back to default mode $options['mode'] ??= $this->mode; return new Session($this, null, $options); } /** * Returns the specified Session object * * @param string $token Session token, either including or without the key * @param string|null $mode Optional transmission mode override */ public function get(string $token, string|null $mode = null): Session { return $this->cache[$token] ??= new Session( $this, $token, ['mode' => $mode ?? $this->mode] ); } /** * Returns the current session based on the configured token transmission mode: * - In `cookie` mode: Gets the session from the cookie * - In `header` mode: Gets the session from the `Authorization` request header * - In `manual` mode: Fails and throws an Exception * * @return \Kirby\Session\Session|null Either the current session or null in case there isn't one * @throws \Kirby\Exception\Exception * @throws \Kirby\Exception\LogicException */ public function current(): Session|null { $token = match ($this->mode) { 'cookie' => $this->tokenFromCookie(), 'header' => $this->tokenFromHeader(), 'manual' => throw new LogicException([ 'key' => 'session.sessions.manualMode', 'fallback' => 'Cannot automatically get current session in manual mode', 'translate' => false, 'httpCode' => 500 ]), // unexpected error that shouldn't occur default => throw new Exception(['translate' => false]) // @codeCoverageIgnore }; // no token was found, no session if (is_string($token) === false) { return null; } // token was found, try to get the session try { return $this->get($token); } catch (Throwable) { return null; } } /** * Returns the current session using the following detection order without using the configured mode: * - Tries to get the session from the `Authorization` request header * - Tries to get the session from the cookie * - Otherwise returns null * * @return \Kirby\Session\Session|null Either the current session or null in case there isn't one */ public function currentDetected(): Session|null { $tokenFromHeader = $this->tokenFromHeader(); $tokenFromCookie = $this->tokenFromCookie(); // prefer header token over cookie token $token = $tokenFromHeader ?? $tokenFromCookie; // no token was found, no session if (is_string($token) === false) { return null; } // token was found, try to get the session try { $mode = is_string($tokenFromHeader) ? 'header' : 'cookie'; return $this->get($token, $mode); } catch (Throwable) { return null; } } /** * Getter for the session store instance * @internal */ public function store(): SessionStore { return $this->store; } /** * Getter for the cookie name * @internal */ public function cookieName(): string { return $this->cookieName; } /** * Deletes all expired sessions * * If the `gcInterval` is configured, this is done automatically * on init of the Sessions object. */ public function collectGarbage(): void { $this->store()->collectGarbage(); } /** * Updates the instance cache with a newly created * session or a session with a regenerated token * * @internal * @param \Kirby\Session\Session $session Session instance to push to the cache */ public function updateCache(Session $session): void { $this->cache[$session->token()] = $session; } /** * Returns the auth token from the cookie */ protected function tokenFromCookie(): string|null { $value = Cookie::get($this->cookieName()); if (is_string($value) === false) { return null; } return $value; } /** * Returns the auth token from the Authorization header */ protected function tokenFromHeader(): string|null { $request = new Request(); $headers = $request->headers(); // check if the header exists at all if ($header = $headers['Authorization'] ?? null) { // check if the header uses the "Session" scheme if (Str::startsWith($header, 'Session ', true) !== true) { return null; } // return the part after the scheme return substr($header, 8); } return null; } }