misuzu/src/Auth/Sessions.php
flash 383e2ed0e0 Rewrote the user information class.
This one took multiple days and it pretty invasive into the core of Misuzu so issue might (will) arise, there's also some features that have gone temporarily missing in the mean time and some inefficiencies introduced that will be fixed again at a later time.
The old class isn't gone entirely because I still have to figure out what I'm gonna do about validation, but for the most part this knocks out one of the "layers of backwards compatibility", as I've been referring to it, and is moving us closer to a future where Flashii actually gets real updates.
If you run into anything that's broken and you're inhibited from reporting it through the forum, do it through chat or mail me at flashii-issues@flash.moe.
2023-08-02 22:12:47 +00:00

286 lines
9.9 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
): array {
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();
$result = $stmt->getResult();
$sessions = [];
while($result->next())
$sessions[] = new SessionInfo($result);
return $sessions;
}
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 new SessionInfo($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()');
}
}