* @link https://getkirby.com * @copyright Bastian Allgeier * @license https://opensource.org/licenses/MIT */ class Sessions { protected $store; protected $mode; protected $cookieName; protected $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($store, array $options = []) { if (is_string($store)) { $this->store = new FileSessionStore($store); } elseif (is_a($store, 'Kirby\Session\SessionStore') === true) { $this->store = $store; } else { throw new InvalidArgumentException([ 'data' => ['method' => 'Sessions::__construct', 'argument' => 'store'], 'translate' => false ]); } $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'])) { throw new InvalidArgumentException([ 'data' => ['method' => 'Sessions::__construct', 'argument' => '$options[\'mode\']'], 'translate' => false ]); } if (!is_string($this->cookieName)) { throw new InvalidArgumentException([ 'data' => ['method' => 'Sessions::__construct', 'argument' => '$options[\'cookieName\']'], 'translate' => false ]); } // trigger automatic garbage collection with the given probability if (is_int($gcInterval) && $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` * @return \Kirby\Session\Session */ public function create(array $options = []) { // fall back to default mode if (!isset($options['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 $mode Optional transmission mode override * @return \Kirby\Session\Session */ public function get(string $token, string $mode = null) { if (isset($this->cache[$token])) { return $this->cache[$token]; } 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 */ public function current() { $token = null; switch ($this->mode) { case 'cookie': $token = $this->tokenFromCookie(); break; case 'header': $token = $this->tokenFromHeader(); break; case 'manual': throw new LogicException([ 'key' => 'session.sessions.manualMode', 'fallback' => 'Cannot automatically get current session in manual mode', 'translate' => false, 'httpCode' => 500 ]); default: // unexpected error that shouldn't occur throw new Exception(['translate' => false]); // @codeCoverageIgnore } // no token was found, no session if (!is_string($token)) { return null; } // token was found, try to get the session try { return $this->get($token); } catch (Throwable $e) { 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() { $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)) { return null; } // token was found, try to get the session try { $mode = (is_string($tokenFromHeader)) ? 'header' : 'cookie'; return $this->get($token, $mode); } catch (Throwable $e) { return null; } } /** * Getter for the session store instance * Used internally * * @return \Kirby\Session\SessionStore */ public function store() { return $this->store; } /** * Getter for the cookie name * Used internally * * @return string */ 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. * * @return void */ public function collectGarbage() { $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) { $this->cache[$session->token()] = $session; } /** * Returns the auth token from the cookie * * @return string|null */ protected function tokenFromCookie() { $value = Cookie::get($this->cookieName()); if (is_string($value)) { return $value; } else { return null; } } /** * Returns the auth token from the Authorization header * * @return string|null */ protected function tokenFromHeader() { $request = new Request(); $headers = $request->headers(); // check if the header exists at all if (!isset($headers['Authorization'])) { return null; } // check if the header uses the "Session" scheme $header = $headers['Authorization']; if (Str::startsWith($header, 'Session ', true) !== true) { return null; } // return the part after the scheme return substr($header, 8); } }