2023-07-28 20:06:12 +00:00
|
|
|
<?php
|
|
|
|
namespace Misuzu\Auth;
|
|
|
|
|
|
|
|
use InvalidArgumentException;
|
|
|
|
use RuntimeException;
|
2023-07-28 20:13:11 +00:00
|
|
|
use Index\XString;
|
2023-07-28 20:06:12 +00:00
|
|
|
use Index\Data\DbStatementCache;
|
|
|
|
use Index\Data\DbTools;
|
|
|
|
use Index\Data\IDbConnection;
|
|
|
|
use Index\Net\IPAddress;
|
|
|
|
use Misuzu\ClientInfo;
|
|
|
|
use Misuzu\Pagination;
|
2023-08-02 22:12:47 +00:00
|
|
|
use Misuzu\Users\UserInfo;
|
2023-07-28 20:06:12 +00:00
|
|
|
|
|
|
|
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 {
|
2023-07-28 20:13:11 +00:00
|
|
|
return XString::random(64);
|
2023-07-28 20:06:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function countSessions(
|
2023-08-02 22:12:47 +00:00
|
|
|
UserInfo|string|null $userInfo = null
|
2023-07-28 20:06:12 +00:00
|
|
|
): int {
|
2023-08-02 22:12:47 +00:00
|
|
|
if($userInfo instanceof UserInfo)
|
|
|
|
$userInfo = $userInfo->getId();
|
2023-07-28 20:06:12 +00:00
|
|
|
|
|
|
|
$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(
|
2023-08-02 22:12:47 +00:00
|
|
|
UserInfo|string|null $userInfo = null,
|
2023-07-28 20:06:12 +00:00
|
|
|
?Pagination $pagination = null
|
|
|
|
): array {
|
2023-08-02 22:12:47 +00:00
|
|
|
if($userInfo instanceof UserInfo)
|
|
|
|
$userInfo = $userInfo->getId();
|
2023-07-28 20:06:12 +00:00
|
|
|
|
|
|
|
$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(
|
2023-08-02 22:12:47 +00:00
|
|
|
UserInfo|string $userInfo,
|
2023-07-28 20:06:12 +00:00
|
|
|
IPAddress|string $remoteAddr,
|
|
|
|
string $countryCode,
|
|
|
|
string $userAgentString,
|
|
|
|
?ClientInfo $clientInfo = null
|
|
|
|
): SessionInfo {
|
2023-08-02 22:12:47 +00:00
|
|
|
if($userInfo instanceof UserInfo)
|
|
|
|
$userInfo = $userInfo->getId();
|
2023-07-28 20:06:12 +00:00
|
|
|
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,
|
2023-08-02 22:12:47 +00:00
|
|
|
UserInfo|string|array|null $userInfos = null
|
2023-07-28 20:06:12 +00:00
|
|
|
): 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) {
|
2023-08-02 22:12:47 +00:00
|
|
|
if($userInfo instanceof UserInfo)
|
|
|
|
$userInfo = $userInfo->getId();
|
2023-07-28 20:06:12 +00:00
|
|
|
elseif(!is_string($userInfo))
|
2023-08-02 22:12:47 +00:00
|
|
|
throw new InvalidArgumentException('$userInfos must be strings or instances of UserInfo.');
|
2023-07-28 20:06:12 +00:00
|
|
|
|
|
|
|
$stmt->addParameter(++$args, $userInfo);
|
|
|
|
}
|
|
|
|
|
|
|
|
$stmt->execute();
|
|
|
|
}
|
|
|
|
|
2023-08-02 22:12:47 +00:00
|
|
|
public function recordSessionActivity(
|
2023-07-28 20:06:12 +00:00
|
|
|
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()');
|
|
|
|
}
|
|
|
|
}
|