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()'); } }