Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,27 @@ A few contract details worth knowing:
- **`window_start` is an unsigned 32-bit epoch bucket** (seconds since
the epoch, rounded down to a window boundary), matching the column's
`int unsigned` type above.

Sessions
--------
Routes with the `session` filter start a session through the
application's session handler (`Neuron\SessionHandlers\SessionHandler`,
which wraps PHP's own save handler, or `DbSessionHandler`, which stores
sessions in a `sessions` table).

- **Session ids are validated.** A request may only bring an id in the
format PHP generates (22 to 256 characters out of `A-Za-z0-9,-`) that
has a stored session. Any other id is ignored and the request gets a
new session. The handlers switch on `session.use_strict_mode` when a
session starts. `SessionHandler` can only check for a stored session
when PHP's save handler is `files`.
- **The `PSID` query parameter is opt-in.** A session id in the query
string is only read after
`\Neuron\SessionHandlers\SessionHandler::setQueryParameterEnabled (true);`
and only when the request has no (valid) session cookie.
- **Sessions expire after `session.gc_maxlifetime` seconds without a
request** (`DbSessionHandler`). An older row is treated as absent and
removed when it is read; garbage collection uses the same lifetime.
Set `session.gc_maxlifetime` before the session starts.
- **`$request->getSession ()->regenerate ()`** gives the session a new
id and keeps its data. Call it when a user logs in.
23 changes: 23 additions & 0 deletions src/Neuron/Net/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ public function disconnect ()
}
}

/**
* Give the active session a new id and remove the stored session of
* the old one. The session data is kept. Call this whenever the
* privileges of the session change, for example when a user logs in.
* Does nothing when no session is active, or when the new session
* cookie could not be sent any more.
* @return bool True if the session has a new id.
*/
public function regenerate ()
{
if (session_status () !== PHP_SESSION_ACTIVE)
{
return false;
}

if (headers_sent () && ini_get ('session.use_cookies'))
{
return false;
}

return session_regenerate_id (true);
}

/**
* Destroy a session
*/
Expand Down
95 changes: 83 additions & 12 deletions src/Neuron/SessionHandlers/DbSessionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,40 @@ class DbSessionHandler
{
/** @var Database $db */
private $db;

/** @var array Session data by id; null for an id without a (live) session. */
private $sessions = array ();

/**
* Has a session that was last written at $setTime outlived its lifetime?
* @param int|string $setTime Unix timestamp of the last write.
* @param int $now
* @param int $maxLifetime In seconds.
* @return bool
*/
public static function isExpired ($setTime, $now, $maxLifetime)
{
return (int) $setTime < (int) $now - (int) $maxLifetime;
}

/**
* Seconds a session may go without being written before it expires:
* session.gc_maxlifetime.
* @return int
*/
protected function getMaxLifetime ()
{
return (int) ini_get ('session.gc_maxlifetime');
}

/**
* @return int
*/
protected function now ()
{
return time ();
}

/* Methods */
#[\ReturnTypeWillChange]
public function open ( $save_path , $name )
Expand All @@ -39,41 +71,80 @@ public function close ( )
#[\ReturnTypeWillChange]
public function destroy ( $session_id )
{
Query::delete ('sessions', array ('id' => $session_id))->execute ();
Query::delete ('sessions', array ('id' => array ($session_id, Query::PARAM_STR)))->execute ();
$this->sessions[$session_id] = null;

return true;
}

#[\ReturnTypeWillChange]
public function gc ( $maxlifetime )
{
Query::delete ('sessions', array ('set_time' => array (time () - 60 * 10, Query::PARAM_NUMBER, '<')))->execute ();
Query::delete ('sessions', array ('set_time' => array ($this->now () - (int) $maxlifetime, Query::PARAM_NUMBER, '<')))->execute ();
return true;
}

#[\ReturnTypeWillChange]
public function read ( $session_id )
{
if (!isset ($this->sessions[$session_id]))
$data = $this->load ($session_id);
return $data === null ? '' : $data;
}

/**
* The data of a stored session that has not expired, or null. A row
* that outlived the lifetime is removed.
* @param string $session_id
* @return string|null
*/
private function load ($session_id)
{
if (!self::isValidSessionId ($session_id))
{
$data = Query::select ('sessions', array ('data'), array ('id' => $session_id))->execute ();
if (count ($data) > 0)
{
$this->sessions[$session_id] = $data[0]['data'];
}
else
return null;
}

if (!array_key_exists ($session_id, $this->sessions))
{
$this->sessions[$session_id] = null;

$rows = Query::select (
'sessions',
array ('data', 'set_time'),
array ('id' => array ($session_id, Query::PARAM_STR))
)->execute ();

if (count ($rows) > 0)
{
$this->sessions[$session_id] = '';
if (self::isExpired ($rows[0]['set_time'], $this->now (), $this->getMaxLifetime ()))
{
$this->destroy ($session_id);
}
else
{
$this->sessions[$session_id] = (string) $rows[0]['data'];
}
}
}

return $this->sessions[$session_id];
}

/**
* @param string $session_id
* @return bool
*/
protected function sessionExists ($session_id)
{
return $this->load ($session_id) !== null;
}

#[\ReturnTypeWillChange]
public function write ( $session_id , $session_data )
{
$this->sessions[$session_id] = $session_data;

$time = time ();
$time = $this->now ();
$this->db->query
("
REPLACE
Expand All @@ -95,4 +166,4 @@ public function write ( $session_id , $session_data )

return true;
}
}
}
Loading
Loading