2022-06-17 17:51:59 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Kirby\Session;
|
|
|
|
|
|
|
|
use Kirby\Exception\Exception;
|
|
|
|
use Kirby\Exception\InvalidArgumentException;
|
|
|
|
use Kirby\Exception\LogicException;
|
|
|
|
use Kirby\Http\Cookie;
|
|
|
|
use Kirby\Http\Request;
|
|
|
|
use Kirby\Toolkit\Str;
|
|
|
|
use Throwable;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sessions - Base class for all session fiddling
|
|
|
|
*
|
|
|
|
* @package Kirby Session
|
|
|
|
* @author Lukas Bestle <lukas@getkirby.com>
|
|
|
|
* @link https://getkirby.com
|
|
|
|
* @copyright Bastian Allgeier
|
|
|
|
* @license https://opensource.org/licenses/MIT
|
|
|
|
*/
|
|
|
|
class Sessions
|
|
|
|
{
|
2025-04-21 18:57:21 +02:00
|
|
|
protected SessionStore $store;
|
|
|
|
protected string $mode;
|
|
|
|
protected string $cookieName;
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2025-04-21 18:57:21 +02:00
|
|
|
protected array $cache = [];
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
/**
|
|
|
|
* 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`
|
|
|
|
*/
|
2025-04-21 18:57:21 +02:00
|
|
|
public function __construct(SessionStore|string $store, array $options = [])
|
2022-08-31 15:02:43 +02:00
|
|
|
{
|
2025-04-21 18:57:21 +02:00
|
|
|
$this->store = match (is_string($store)) {
|
|
|
|
true => new FileSessionStore($store),
|
|
|
|
default => $store
|
|
|
|
};
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
$this->mode = $options['mode'] ?? 'cookie';
|
|
|
|
$this->cookieName = $options['cookieName'] ?? 'kirby_session';
|
|
|
|
$gcInterval = $options['gcInterval'] ?? 100;
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
// validate options
|
2025-04-21 18:57:21 +02:00
|
|
|
if (in_array($this->mode, ['cookie', 'header', 'manual']) === false) {
|
2022-08-31 15:02:43 +02:00
|
|
|
throw new InvalidArgumentException([
|
2025-04-21 18:57:21 +02:00
|
|
|
'data' => [
|
|
|
|
'method' => 'Sessions::__construct',
|
|
|
|
'argument' => '$options[\'mode\']'
|
|
|
|
],
|
2022-08-31 15:02:43 +02:00
|
|
|
'translate' => false
|
|
|
|
]);
|
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
// trigger automatic garbage collection with the given probability
|
2025-04-21 18:57:21 +02:00
|
|
|
if (is_int($gcInterval) === true && $gcInterval > 0) {
|
2022-08-31 15:02:43 +02:00
|
|
|
// convert the interval into a probability between 0 and 1
|
|
|
|
$gcProbability = 1 / $gcInterval;
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
// generate a random number
|
|
|
|
$random = mt_rand(1, 10000);
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
// $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([
|
2025-04-21 18:57:21 +02:00
|
|
|
'data' => [
|
|
|
|
'method' => 'Sessions::__construct',
|
|
|
|
'argument' => '$options[\'gcInterval\']'
|
|
|
|
],
|
2022-08-31 15:02:43 +02:00
|
|
|
'translate' => false
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
/**
|
|
|
|
* 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`
|
|
|
|
*/
|
2025-04-21 18:57:21 +02:00
|
|
|
public function create(array $options = []): Session
|
2022-08-31 15:02:43 +02:00
|
|
|
{
|
|
|
|
// fall back to default mode
|
2022-12-19 14:56:05 +01:00
|
|
|
$options['mode'] ??= $this->mode;
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
return new Session($this, null, $options);
|
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
/**
|
|
|
|
* Returns the specified Session object
|
|
|
|
*
|
|
|
|
* @param string $token Session token, either including or without the key
|
2025-04-21 18:57:21 +02:00
|
|
|
* @param string|null $mode Optional transmission mode override
|
2022-08-31 15:02:43 +02:00
|
|
|
*/
|
2025-04-21 18:57:21 +02:00
|
|
|
public function get(string $token, string|null $mode = null): Session
|
2022-08-31 15:02:43 +02:00
|
|
|
{
|
2025-04-21 18:57:21 +02:00
|
|
|
return $this->cache[$token] ??= new Session(
|
|
|
|
$this,
|
|
|
|
$token,
|
|
|
|
['mode' => $mode ?? $this->mode]
|
|
|
|
);
|
2022-08-31 15:02:43 +02:00
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
/**
|
|
|
|
* 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
|
2022-12-19 14:56:05 +01:00
|
|
|
* @throws \Kirby\Exception\Exception
|
|
|
|
* @throws \Kirby\Exception\LogicException
|
2022-08-31 15:02:43 +02:00
|
|
|
*/
|
2025-04-21 18:57:21 +02:00
|
|
|
public function current(): Session|null
|
2022-08-31 15:02:43 +02:00
|
|
|
{
|
2022-12-19 14:56:05 +01:00
|
|
|
$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
|
|
|
|
};
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
// no token was found, no session
|
2022-12-19 14:56:05 +01:00
|
|
|
if (is_string($token) === false) {
|
2022-08-31 15:02:43 +02:00
|
|
|
return null;
|
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
// token was found, try to get the session
|
|
|
|
try {
|
|
|
|
return $this->get($token);
|
2022-12-19 14:56:05 +01:00
|
|
|
} catch (Throwable) {
|
2022-08-31 15:02:43 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2025-04-21 18:57:21 +02:00
|
|
|
public function currentDetected(): Session|null
|
2022-08-31 15:02:43 +02:00
|
|
|
{
|
|
|
|
$tokenFromHeader = $this->tokenFromHeader();
|
|
|
|
$tokenFromCookie = $this->tokenFromCookie();
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
// prefer header token over cookie token
|
|
|
|
$token = $tokenFromHeader ?? $tokenFromCookie;
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
// no token was found, no session
|
2025-04-21 18:57:21 +02:00
|
|
|
if (is_string($token) === false) {
|
2022-08-31 15:02:43 +02:00
|
|
|
return null;
|
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
// token was found, try to get the session
|
|
|
|
try {
|
2025-04-21 18:57:21 +02:00
|
|
|
$mode = is_string($tokenFromHeader) ? 'header' : 'cookie';
|
2022-08-31 15:02:43 +02:00
|
|
|
return $this->get($token, $mode);
|
2022-12-19 14:56:05 +01:00
|
|
|
} catch (Throwable) {
|
2022-08-31 15:02:43 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
/**
|
|
|
|
* Getter for the session store instance
|
2025-04-21 18:57:21 +02:00
|
|
|
* @internal
|
2022-08-31 15:02:43 +02:00
|
|
|
*/
|
2025-04-21 18:57:21 +02:00
|
|
|
public function store(): SessionStore
|
2022-08-31 15:02:43 +02:00
|
|
|
{
|
|
|
|
return $this->store;
|
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
/**
|
|
|
|
* Getter for the cookie name
|
2025-04-21 18:57:21 +02:00
|
|
|
* @internal
|
2022-08-31 15:02:43 +02:00
|
|
|
*/
|
|
|
|
public function cookieName(): string
|
|
|
|
{
|
|
|
|
return $this->cookieName;
|
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
/**
|
|
|
|
* Deletes all expired sessions
|
|
|
|
*
|
|
|
|
* If the `gcInterval` is configured, this is done automatically
|
|
|
|
* on init of the Sessions object.
|
|
|
|
*/
|
2025-04-21 18:57:21 +02:00
|
|
|
public function collectGarbage(): void
|
2022-08-31 15:02:43 +02:00
|
|
|
{
|
|
|
|
$this->store()->collectGarbage();
|
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2025-04-21 18:57:21 +02:00
|
|
|
public function updateCache(Session $session): void
|
2022-08-31 15:02:43 +02:00
|
|
|
{
|
|
|
|
$this->cache[$session->token()] = $session;
|
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
/**
|
|
|
|
* Returns the auth token from the cookie
|
|
|
|
*/
|
2025-04-21 18:57:21 +02:00
|
|
|
protected function tokenFromCookie(): string|null
|
2022-08-31 15:02:43 +02:00
|
|
|
{
|
|
|
|
$value = Cookie::get($this->cookieName());
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-12-19 14:56:05 +01:00
|
|
|
if (is_string($value) === false) {
|
2022-08-31 15:02:43 +02:00
|
|
|
return null;
|
|
|
|
}
|
2022-12-19 14:56:05 +01:00
|
|
|
|
|
|
|
return $value;
|
2022-08-31 15:02:43 +02:00
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
/**
|
|
|
|
* Returns the auth token from the Authorization header
|
|
|
|
*/
|
2025-04-21 18:57:21 +02:00
|
|
|
protected function tokenFromHeader(): string|null
|
2022-08-31 15:02:43 +02:00
|
|
|
{
|
|
|
|
$request = new Request();
|
|
|
|
$headers = $request->headers();
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2022-08-31 15:02:43 +02:00
|
|
|
// check if the header exists at all
|
2025-04-21 18:57:21 +02:00
|
|
|
if ($header = $headers['Authorization'] ?? null) {
|
|
|
|
// check if the header uses the "Session" scheme
|
|
|
|
if (Str::startsWith($header, 'Session ', true) !== true) {
|
|
|
|
return null;
|
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2025-04-21 18:57:21 +02:00
|
|
|
// return the part after the scheme
|
|
|
|
return substr($header, 8);
|
2022-08-31 15:02:43 +02:00
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
|
2025-04-21 18:57:21 +02:00
|
|
|
return null;
|
2022-08-31 15:02:43 +02:00
|
|
|
}
|
2022-06-17 17:51:59 +02:00
|
|
|
}
|