misuzu/src/Auth/Sessions.php

280 lines
9.8 KiB
PHP

<?php
namespace Misuzu\Auth;
use InvalidArgumentException;
use RuntimeException;
use Index\XString;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Index\Net\IPAddress;
use Misuzu\ClientInfo;
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
class Sessions {
private IDbConnection $dbConn;
private DbStatementCache $cache;
public function __construct(IDbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}
public static function generateToken(): string {
return XString::random(64);
}
public function countSessions(
UserInfo|string|null $userInfo = null
): int {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null;
//$args = 0;
$query = 'SELECT COUNT(*) FROM msz_sessions';
if($hasUserInfo) {
//++$args;
$query .= ' WHERE user_id = ?';
}
$args = 0;
$stmt = $this->cache->get($query);
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo);
$stmt->execute();
$result = $stmt->getResult();
$count = 0;
if($result->next())
$count = $result->getInteger(0);
return $count;
}
public function getSessions(
UserInfo|string|null $userInfo = null,
?Pagination $pagination = null
): iterable {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
$hasUserInfo = $userInfo !== null;
$hasPagination = $pagination !== null;
//$args = 0;
$query = 'SELECT session_id, user_id, session_key, INET6_NTOA(session_ip), INET6_NTOA(session_ip_last), session_user_agent, session_client_info, session_country, UNIX_TIMESTAMP(session_expires), session_expires_bump, UNIX_TIMESTAMP(session_created), UNIX_TIMESTAMP(session_active) FROM msz_sessions';
if($hasUserInfo) {
//++$args;
$query .= ' WHERE user_id = ?';
}
$query .= ' ORDER BY session_active DESC';
if($hasPagination)
$query .= ' LIMIT ? OFFSET ?';
$args = 0;
$stmt = $this->cache->get($query);
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo);
if($hasPagination) {
$stmt->addParameter(++$args, $pagination->getRange());
$stmt->addParameter(++$args, $pagination->getOffset());
}
$stmt->execute();
return $stmt->getResult()->getIterator(SessionInfo::fromResult(...));
}
public function getSession(
?string $sessionId = null,
?string $sessionToken = null
): SessionInfo {
if($sessionId === null && $sessionToken === null)
throw new InvalidArgumentException('At least one argument must be specified.');
if($sessionId !== null && $sessionToken !== null)
throw new InvalidArgumentException('Only one argument may be specified.');
$hasSessionId = $sessionId !== null;
$hasSessionToken = $sessionToken !== null;
$value = null;
$query = 'SELECT session_id, user_id, session_key, INET6_NTOA(session_ip), INET6_NTOA(session_ip_last), session_user_agent, session_client_info, session_country, UNIX_TIMESTAMP(session_expires), session_expires_bump, UNIX_TIMESTAMP(session_created), UNIX_TIMESTAMP(session_active) FROM msz_sessions';
if($hasSessionId) {
$query .= ' WHERE session_id = ?';
$value = $sessionId;
} elseif($hasSessionToken) {
$query .= ' WHERE session_key = ?';
$value = $sessionToken;
}
$stmt = $this->cache->get($query);
$stmt->addParameter(1, $value);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('Session not found.');
return SessionInfo::fromResult($result);
}
public function createSession(
UserInfo|string $userInfo,
IPAddress|string $remoteAddr,
string $countryCode,
string $userAgentString,
?ClientInfo $clientInfo = null
): SessionInfo {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$sessionToken = self::generateToken();
$clientInfo = json_encode($clientInfo ?? ClientInfo::parse($userAgentString));
$stmt = $this->cache->get('INSERT INTO msz_sessions (user_id, session_key, session_ip, session_user_agent, session_client_info, session_country, session_expires) VALUES (?, ?, INET6_ATON(?), ?, ?, ?, NOW() + INTERVAL 1 MONTH)');
$stmt->addParameter(1, $userInfo);
$stmt->addParameter(2, $sessionToken);
$stmt->addParameter(3, $remoteAddr);
$stmt->addParameter(4, $userAgentString);
$stmt->addParameter(5, $clientInfo);
$stmt->addParameter(6, $countryCode);
$stmt->execute();
return $this->getSession(sessionId: (string)$this->dbConn->getLastInsertId());
}
public function deleteSessions(
SessionInfo|string|array|null $sessionInfos = null,
string|array|null $sessionTokens = null,
UserInfo|string|array|null $userInfos = null
): void {
$hasSessionInfos = $sessionInfos !== null;
$hasSessionTokens = $sessionTokens !== null;
$hasUserInfos = $userInfos !== null;
$args = 0;
$query = 'DELETE FROM msz_sessions';
if($hasSessionInfos) {
if(!is_array($sessionInfos))
$sessionInfos = [$sessionInfos];
if(empty($sessionInfos))
$hasSessionInfos = false;
else {
++$args;
$query .= sprintf(
' WHERE session_id IN (%s)',
DbTools::prepareListString($sessionInfos)
);
}
}
if($hasSessionTokens) {
if(!is_array($sessionTokens))
$sessionTokens = [$sessionTokens];
if(empty($sessionTokens))
$hasSessionTokens = false;
else
$query .= sprintf(
' %s session_key IN (%s)',
++$args > 1 ? 'OR' : 'WHERE',
DbTools::prepareListString($sessionTokens)
);
}
if($hasUserInfos) {
if(!is_array($userInfos))
$userInfos = [$userInfos];
if(empty($userInfos))
$hasUserInfos = false;
else
$query .= sprintf(
' %s user_id IN (%s)',
++$args > 1 ? 'OR' : 'WHERE',
DbTools::prepareListString($userInfos)
);
}
if(!$hasSessionInfos && !$hasSessionTokens && !$hasUserInfos)
throw new InvalidArgumentException('At least one argument must be specified.');
$args = 0;
$stmt = $this->cache->get($query);
if($hasSessionInfos)
foreach($sessionInfos as $sessionInfo) {
if($sessionInfo instanceof SessionInfo)
$sessionInfo = $sessionInfo->getId();
elseif(!is_string($sessionInfo))
throw new InvalidArgumentException('$sessionInfos must be strings or instances of SessionInfo.');
$stmt->addParameter(++$args, $sessionInfo);
}
if($hasSessionTokens)
foreach($sessionTokens as $sessionToken) {
if(!is_string($sessionToken))
throw new InvalidArgumentException('$sessionTokens must be strings.');
$stmt->addParameter(++$args, $sessionToken);
}
if($hasUserInfos)
foreach($userInfos as $userInfo) {
if($userInfo instanceof UserInfo)
$userInfo = $userInfo->getId();
elseif(!is_string($userInfo))
throw new InvalidArgumentException('$userInfos must be strings or instances of UserInfo.');
$stmt->addParameter(++$args, $userInfo);
}
$stmt->execute();
}
public function recordSessionActivity(
SessionInfo|string|null $sessionInfo = null,
?string $sessionToken = null,
IPAddress|string|null $remoteAddr = null
): void {
if($sessionInfo === null && $sessionToken === null)
throw new InvalidArgumentException('Either $sessionInfo or $sessionToken needs to be set.');
if($sessionInfo !== null && $sessionToken !== null)
throw new InvalidArgumentException('Only one of $sessionInfo and $sessionToken may be set at once.');
if($sessionInfo instanceof SessionInfo)
$sessionInfo = $sessionInfo->getId();
if($remoteAddr instanceof IPAddress)
$remoteAddr = (string)$remoteAddr;
$hasSessionInfo = $sessionInfo !== null;
$hasSessionToken = $sessionToken !== null;
$value = null;
$query = 'UPDATE msz_sessions SET session_ip_last = COALESCE(INET6_ATON(?), session_ip_last), session_active = NOW(), session_expires = IF(session_expires_bump, NOW() + INTERVAL 1 MONTH, session_expires)';
if($hasSessionInfo) {
$query .= ' WHERE session_id = ?';
$value = $sessionInfo;
} elseif($hasSessionToken) {
$query .= ' WHERE session_key = ?';
$value = $sessionToken;
} else throw new RuntimeException('Failsafe to prevent all sessions from being updated at once somehow.');
$stmt = $this->cache->get($query);
$stmt->addParameter(1, $remoteAddr);
$stmt->addParameter(2, $value);
$stmt->execute();
}
public function purgeExpiredSessions(): void {
$this->dbConn->execute('DELETE FROM msz_sessions WHERE session_expires <= NOW()');
}
}