Files
development/www/lib/CoreLibs/Create/Session.php
Clemens Schwaighofer 8d0036eaac Fix phpdoc return types
2024-12-23 11:26:50 +09:00

615 lines
16 KiB
PHP

<?php
/*
* AUTHOR: Clemens Schwaighofer
* DESCRIPTION:
* start a php sesseion
* name can be given via startSession parameter
* if not set tries to read $SET_SESSION_NAME from global
* else will use default set in php.ini
*/
declare(strict_types=1);
namespace CoreLibs\Create;
class Session
{
/** @var string current session name */
private string $session_name = '';
/** @var string current session id */
private string $session_id = '';
/** @var bool flag auto write close */
private bool $auto_write_close = false;
/** @var string regenerate option, default never */
private string $regenerate = 'never';
/** @var int regenerate interval either 1 to 100 for random or 0 to 3600 for interval */
private int $regenerate_interval = 0;
/** @var array<string> allowed session id regenerate (rotate) options */
private const ALLOWED_REGENERATE_OPTIONS = ['none', 'random', 'interval'];
/** @var int default random interval */
public const DEFAULT_REGENERATE_RANDOM = 100;
/** @var int default rotate internval in minutes */
public const DEFAULT_REGENERATE_INTERVAL = 5 * 60;
/** @var int maximum time for regenerate interval is one hour */
public const MAX_REGENERATE_INTERAL = 60 * 60;
/**
* init a session, if array is empty or array does not have session_name set
* then no auto init is run
*
* @param string $session_name if set and not empty, will start session
* @param array{auto_write_close?:bool,session_strict?:bool,regenerate?:string,regenerate_interval?:int} $options
*/
public function __construct(
string $session_name,
array $options = []
) {
$this->setOptions($options);
$this->initSession($session_name);
}
// MARK: private methods
/**
* set session class options
*
* @param array{auto_write_close?:bool,session_strict?:bool,regenerate?:string,regenerate_interval?:int} $options
* @return void
*/
private function setOptions(array $options): void
{
if (
!isset($options['auto_write_close']) ||
!is_bool($options['auto_write_close'])
) {
$options['auto_write_close'] = false;
}
$this->auto_write_close = $options['auto_write_close'];
if (
!isset($options['session_strict']) ||
!is_bool($options['session_strict'])
) {
$options['session_strict'] = true;
}
// set strict options, on not started sessiononly
if (
$options['session_strict'] &&
$this->getSessionStatus() === PHP_SESSION_NONE
) {
// use cookies to store session IDs
ini_set('session.use_cookies', 1);
// use cookies only (do not send session IDs in URLs)
ini_set('session.use_only_cookies', 1);
// do not send session IDs in URLs
ini_set('session.use_trans_sid', 0);
}
// session regenerate id options
if (
empty($options['regenerate']) ||
!in_array($options['regenerate'], self::ALLOWED_REGENERATE_OPTIONS)
) {
$options['regenerate'] = 'never';
}
$this->regenerate = (string)$options['regenerate'];
// for regenerate: 'random' (default 100)
// regenerate_interval must be between (1 = always) and 100 (1 in 100)
// for regenerate: 'interval' (default 5min)
// regenerate_interval must be 0 = always, to 3600 (every hour)
if (
$options['regenerate'] == 'random' &&
(
!isset($options['regenerate_interval']) ||
!is_numeric($options['regenerate_interval']) ||
$options['regenerate_interval'] < 0 ||
$options['regenerate_interval'] > 100
)
) {
$options['regenerate_interval'] = self::DEFAULT_REGENERATE_RANDOM;
}
if (
$options['regenerate'] == 'interval' &&
(
!isset($options['regenerate_interval']) ||
!is_numeric($options['regenerate_interval']) ||
$options['regenerate_interval'] < 1 ||
$options['regenerate_interval'] > self::MAX_REGENERATE_INTERAL
)
) {
$options['regenerate_interval'] = self::DEFAULT_REGENERATE_INTERVAL;
}
$this->regenerate_interval = (int)($options['regenerate_interval'] ?? 0);
}
/**
* Start session
* startSession should be called for complete check
* If this is called without any name set before the php.ini name is
* used.
*
* @return void
*/
private function startSessionCall(): void
{
session_start();
}
/**
* get current set session id or false if none started
*
* @return string|false
*/
public function getSessionIdCall(): string|false
{
return session_id();
}
/**
* automatically closes a session if the auto write close flag is set
*
* @return bool
*/
private function closeSessionCall(): bool
{
if ($this->auto_write_close) {
return $this->writeClose();
}
return false;
}
// MARK: regenerate session
/**
* auto rotate session id
*
* @return void
* @throws \RuntimeException failure to regenerate session id
* @throws \UnexpectedValueException failed to get new session id
* @throws \RuntimeException failed to set new sesson id
* @throws \UnexpectedValueException new session id generated does not match the new set one
*/
private function sessionRegenerateSessionId()
{
// never
if ($this->regenerate == 'never') {
return;
}
// regenerate
if (
!(
// is not session obsolete
empty($_SESSION['SESSION_REGENERATE_OBSOLETE']) &&
(
(
// random
$this->regenerate == 'random' &&
mt_rand(1, $this->regenerate_interval) == 1
) || (
// interval type
$this->regenerate == 'interval' &&
($_SESSION['SESSION_REGENERATE_TIMESTAMP'] ?? 0) + $this->regenerate_interval < time()
)
)
)
) {
return;
}
// Set current session to expire in 1 minute
$_SESSION['SESSION_REGENERATE_OBSOLETE'] = true;
$_SESSION['SESSION_REGENERATE_EXPIRES'] = time() + 60;
$_SESSION['SESSION_REGENERATE_TIMESTAMP'] = time();
// Create new session without destroying the old one
if (session_regenerate_id(false) === false) {
throw new \RuntimeException('[SESSION] Session id regeneration failed', 1);
}
// Grab current session ID and close both sessions to allow other scripts to use them
if (false === ($new_session_id = $this->getSessionIdCall())) {
throw new \UnexpectedValueException('[SESSION] getSessionIdCall did not return a session id', 2);
}
$this->writeClose();
// Set session ID to the new one, and start it back up again
if (($get_new_session_id = session_id($new_session_id)) === false) {
throw new \RuntimeException('[SESSION] set session_id failed', 3);
}
if ($get_new_session_id != $new_session_id) {
throw new \UnexpectedValueException('[SESSION] new session id does not match the new set one', 4);
}
$this->session_id = $new_session_id;
$this->startSessionCall();
// Don't want this one to expire
unset($_SESSION['SESSION_REGENERATE_OBSOLETE']);
unset($_SESSION['SESSION_REGENERATE_EXPIRES']);
}
// MARK: session validation
/**
* check if session name is valid
*
* As from PHP 8.1/8.0/7.4 error
* INVALID CHARS: =,; \t\r\n\013\014
* NOTE: using . will fail even thought valid
* we allow only alphanumeric with - (dash) and 1 to 128 characters
*
* @param string $session_name any string, not null
* @return bool True for valid, False for invalid
*/
public static function checkValidSessionName(string $session_name): bool
{
// check
if (
// must only have those
!preg_match('/^[-a-zA-Z0-9]{1,128}$/', $session_name) ||
// cannot be only numbers
preg_match('/^[0-9]+$/', $session_name)
) {
return false;
}
return true;
}
/**
* validate _SESSION key, must be valid variable
*
* @param int|float|string $key
* @return true
*/
private function checkValidSessionEntryKey(int|float|string $key): true
{
if (!is_string($key) || is_numeric($key)) {
throw new \UnexpectedValueException(
'[SESSION] Given key for _SESSION is not a valid value for a varaible: ' . $key,
1
);
}
return true;
}
// MARK: init session (on class start)
/**
* stinitart session with given session name if set
* aborts on command line or if sessions are not enabled
* also aborts if session cannot be started
* On sucess returns the session id
*
* @param string $session_name
* @return void
*/
private function initSession(string $session_name): void
{
// we can't start sessions on command line
if ($this->checkCliStatus()) {
throw new \RuntimeException('[SESSION] No sessions in php cli', 1);
}
// if session are OFF
if ($this->getSessionStatus() === PHP_SESSION_DISABLED) {
throw new \RuntimeException('[SESSION] Sessions are disabled', 2);
}
// session_status
// initial the session if there is no session running already
if (!$this->checkActiveSession()) {
// invalid session name, abort
if (!$this->checkValidSessionName($session_name)) {
throw new \UnexpectedValueException('[SESSION] Invalid session name: ' . $this->session_name, 3);
}
// set session name
$this->session_name = $session_name;
session_name($this->session_name);
// start session
$this->startSessionCall();
// if we faild to start the session
if (!$this->checkActiveSession()) {
throw new \RuntimeException('[SESSION] Failed to activate session', 5);
}
if (
!empty($_SESSION['SESSION_REGENERATE_OBSOLETE']) &&
!empty($_SESSION['SESSION_REGENERATE_EXPIRES']) && $_SESSION['SESSION_REGENERATE_EXPIRES'] < time()
) {
$this->sessionDestroy();
throw new \RuntimeException('[SESSION] Expired session found', 6);
}
} elseif ($session_name != $this->getSessionName()) {
throw new \UnexpectedValueException(
'[SESSION] Another session exists with a different name: ' . $this->getSessionName(),
4
);
}
// check session id
if (false === ($session_id = $this->getSessionIdCall())) {
throw new \UnexpectedValueException('[SESSION] getSessionIdCall did not return a session id', 7);
}
// set session id
$this->session_id = $session_id;
// run session id re-create from time to time
$this->sessionRegenerateSessionId();
// if flagged auto close, write close session
if ($this->auto_write_close) {
$this->writeClose();
}
}
// MARK: public set/get status
/**
* start session, will only run after initSession
*
* @return bool True if started, False if alrady running
*/
public function restartSession(): bool
{
if (!$this->checkActiveSession()) {
if (empty($this->session_name)) {
throw new \RuntimeException('[SESSION] Cannot restart session without a session name', 1);
}
$this->startSessionCall();
return true;
}
return false;
}
/**
* current set session id
*
* @return string
*/
public function getSessionId(): string
{
return $this->session_id;
}
/**
* set the auto write close flag
*
* @param bool $flag
* @return Session
*/
public function setAutoWriteClose(bool $flag): Session
{
$this->auto_write_close = $flag;
return $this;
}
/**
* return the auto write close flag
*
* @return bool
*/
public function checkAutoWriteClose(): bool
{
return $this->auto_write_close;
}
/**
* get set session name or false if none started
*
* @return string|bool
*/
public function getSessionName(): string|bool
{
return session_name();
}
/**
* Checks if there is an active session.
* Does not check if we can have a session
*
* @return bool True if there is an active session, else false
*/
public function checkActiveSession(): bool
{
if ($this->getSessionStatus() === PHP_SESSION_ACTIVE) {
return true;
} else {
return false;
}
}
/**
* check if we are in CLI, we set this, so we can mock this
* Not this is just a wrapper for the static System::checkCLI call
*
* @return bool True if we are in a CLI enviroment, or false for everything else
*/
public function checkCliStatus(): bool
{
return \CoreLibs\Get\System::checkCLI();
}
/**
* get session status
* PHP_SESSION_DISABLED if sessions are disabled.
* PHP_SESSION_NONE if sessions are enabled, but none exists.
* PHP_SESSION_ACTIVE if sessions are enabled, and one exists.
*
* https://www.php.net/manual/en/function.session-status.php
*
* @return int See possible return int values above
*/
public function getSessionStatus(): int
{
return session_status();
}
// MARK: write close session
/**
* unlock the session file, so concurrent AJAX requests can be done
* NOTE: after this has been called, no changes in _SESSION will be stored
* NOTE: a new session with a different name can be started after this one is called
* if problem, run ob_flush() and flush() too
*
* @return bool True und sucess, false on failure
*/
public function writeClose(): bool
{
return session_write_close();
}
// MARK: session close and clean up
/**
* Proper destroy a session
* - unset the _SESSION array
* - unset cookie if cookie on and we have not strict mode
* - unset session_name and session_id internal vars
* - destroy session
*
* @return bool True on successful session destroy
*/
public function sessionDestroy(): bool
{
// abort to false if not unsetable
if (!session_unset()) {
return false;
}
$this->clear();
if (
ini_get('session.use_cookies') &&
!ini_get('session.use_strict_mode')
) {
$session_name = $this->getSessionName();
if ($session_name === false) {
$session_name = '';
}
$params = session_get_cookie_params();
setcookie(
(string)$session_name,
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
// unset internal vars
$this->session_name = '';
$this->session_id = '';
return session_destroy();
}
// MARK: _SESSION set/unset methods
/**
* unset all _SESSION entries
*
* @return void
*/
public function clear(): void
{
$this->restartSession();
if (!session_unset()) {
throw new \RuntimeException('[SESSION] Cannot unset session vars', 1);
}
if (!empty($_SESSION)) {
$_SESSION = [];
}
$this->closeSessionCall();
}
/**
* set _SESSION entry 'name' with any value
*
* @param string $name array name in _SESSION
* @param mixed $value value to set (can be anything)
* @return Session
*/
public function set(string $name, mixed $value): Session
{
$this->checkValidSessionEntryKey($name);
$this->restartSession();
$_SESSION[$name] = $value;
$this->closeSessionCall();
return $this;
}
/**
* set many session entries in one set
*
* @param array<string,mixed> $set key is the key in the _SESSION, value is any data to set
* @return void
*/
public function setMany(array $set): void
{
$this->restartSession();
// skip any that are not valid
foreach ($set as $key => $value) {
$this->checkValidSessionEntryKey($key);
$_SESSION[$key] = $value;
}
$this->closeSessionCall();
}
/**
* get _SESSION 'name' entry or empty string if not set
*
* @param string $name value key to get from _SESSION
* @return mixed value stored in _SESSION, if not found set to null
*/
public function get(string $name): mixed
{
return $_SESSION[$name] ?? null;
}
/**
* get multiple session entries
*
* @param array<string> $set
* @return array<string,mixed>
*/
public function getMany(array $set): array
{
return array_intersect_key($_SESSION, array_flip($set));
}
/**
* Check if a name is set in the _SESSION array
*
* @param string $name Name to check for
* @return bool True for set, False fornot set
*/
public function isset(string $name): bool
{
return isset($_SESSION[$name]);
}
/**
* unset one _SESSION entry 'name' if exists
*
* @param string $name _SESSION key name to remove
* @return Session
*/
public function unset(string $name): Session
{
if (!isset($_SESSION[$name])) {
return $this;
}
$this->restartSession();
unset($_SESSION[$name]);
$this->closeSessionCall();
return $this;
}
/**
* reset many session entry
*
* @param array<string> $set list of session keys to reset
* @return void
*/
public function unsetMany(array $set): void
{
$this->restartSession();
foreach ($set as $key) {
if (!isset($_SESSION[$key])) {
continue;
}
unset($_SESSION[$key]);
}
$this->closeSessionCall();
}
}
// __END__