Cockatrice/servatrice/src/servatrice_database_interface.cpp
ebbit1q 3ec9ae9772
Some checks failed
Build Desktop / Configure (push) Has been cancelled
Build Docker Image / amd64 & arm64 (push) Has been cancelled
Build Desktop / Debian 11 (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Fedora 42 (push) Has been cancelled
Build Desktop / Servatrice_Debian 11 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 22.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 15 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
fix compiling on arch (#6768)
* fix compiling on arch

* redo all the logging in affected files
2026-04-05 21:52:46 +02:00

1462 lines
54 KiB
C++

#include "servatrice_database_interface.h"
#include "servatrice.h"
#include "serversocketinterface.h"
#include "settingscache.h"
#include <QChar>
#include <QDateTime>
#include <QDebug>
#include <QLoggingCategory>
#include <QSqlError>
#include <QSqlQuery>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/protocol/pb/game_replay.pb.h>
#include <libcockatrice/utility/passwordhasher.h>
inline Q_LOGGING_CATEGORY(DatabaseInterfaceLog, "database_interface");
Servatrice_DatabaseInterface::Servatrice_DatabaseInterface(int _instanceId, Servatrice *_server)
: instanceId(_instanceId), sqlDatabase(QSqlDatabase()), server(_server)
{
}
Servatrice_DatabaseInterface::~Servatrice_DatabaseInterface()
{
// reset all prepared statements
qDeleteAll(preparedStatements);
preparedStatements.clear();
sqlDatabase.close();
}
void Servatrice_DatabaseInterface::initDatabase(const QSqlDatabase &_sqlDatabase)
{
if (_sqlDatabase.isValid()) {
sqlDatabase = QSqlDatabase::cloneDatabase(_sqlDatabase, "pool_" + QString::number(instanceId));
openDatabase();
}
}
bool Servatrice_DatabaseInterface::initDatabase(const QString &type,
const QString &hostName,
const QString &databaseName,
const QString &userName,
const QString &password)
{
sqlDatabase = QSqlDatabase::addDatabase(type, "main");
sqlDatabase.setHostName(hostName);
sqlDatabase.setDatabaseName(databaseName);
sqlDatabase.setUserName(userName);
sqlDatabase.setPassword(password);
return openDatabase();
}
bool Servatrice_DatabaseInterface::openDatabase()
{
if (sqlDatabase.isOpen())
sqlDatabase.close();
const QString poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId);
qCDebug(DatabaseInterfaceLog).noquote() << poolStr << "Opening database...";
if (!sqlDatabase.open()) {
qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database:" << sqlDatabase.lastError().text();
return false;
}
QSqlQuery *versionQuery = prepareQuery("select version from {prefix}_schema_version limit 1");
if (!execSqlQuery(versionQuery)) {
qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database: unable to load database schema version"
<< "(hint: ensure the cockatrice_schema_version exists)";
return false;
}
if (versionQuery->next()) {
const int dbversion = versionQuery->value(0).toInt();
const int expectedversion = DATABASE_SCHEMA_VERSION;
if (dbversion < expectedversion) {
qCCritical(DatabaseInterfaceLog) << poolStr
<< "Error opening database: the database schema version is too old, you "
"need to run the migrations to update it from version"
<< dbversion << "to version" << expectedversion;
return false;
} else if (dbversion > expectedversion) {
qCCritical(DatabaseInterfaceLog) << poolStr << "Error opening database: the database schema version"
<< dbversion << "is too new, you need to update servatrice"
<< "(this servatrice actually uses version" << expectedversion << ")";
return false;
}
} else {
qCCritical(DatabaseInterfaceLog) << poolStr
<< "Error opening database: unable to load database schema version (hint: "
"ensure the cockatrice_schema_version contains a single record)";
return false;
}
// reset all prepared statements
qDeleteAll(preparedStatements);
preparedStatements.clear();
return true;
}
bool Servatrice_DatabaseInterface::checkSql()
{
if (!sqlDatabase.isValid()) {
return false;
}
auto query = QSqlQuery(sqlDatabase);
if (query.exec("select 1") && !query.isActive()) {
return openDatabase();
}
if (query.lastError().isValid()) {
const auto &poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId);
qCCritical(DatabaseInterfaceLog) << poolStr << "Error executing query:" << query.lastError().text();
sqlDatabase.close();
return openDatabase();
}
return true;
}
QSqlQuery *Servatrice_DatabaseInterface::prepareQuery(const QString &queryText)
{
if (preparedStatements.contains(queryText)) {
return preparedStatements.value(queryText);
}
QString prefixedQueryText = queryText;
prefixedQueryText.replace("{prefix}", server->getDbPrefix());
auto *query = new QSqlQuery(sqlDatabase);
query->prepare(prefixedQueryText);
preparedStatements.insert(queryText, query);
return query;
}
bool Servatrice_DatabaseInterface::execSqlQuery(QSqlQuery *query)
{
if (query->exec())
return true;
const QString poolStr = instanceId == -1 ? QString("main") : QString("pool %1").arg(instanceId);
qCCritical(DatabaseInterfaceLog) << poolStr << "Error executing query:" << query->lastError().text();
sqlDatabase.close();
openDatabase();
return false;
}
bool Servatrice_DatabaseInterface::usernameIsValid(const QString &user, QString &error)
{
int minNameLength = settingsCache->value("users/minnamelength", 6).toInt();
if (minNameLength < 1)
minNameLength = 1;
int maxNameLength = settingsCache->value("users/maxnamelength", 12).toInt();
bool allowLowercase = settingsCache->value("users/allowlowercase", true).toBool();
bool allowUppercase = settingsCache->value("users/allowuppercase", true).toBool();
bool allowNumerics = settingsCache->value("users/allownumerics", true).toBool();
bool allowPunctuationPrefix = settingsCache->value("users/allowpunctuationprefix", false).toBool();
QString allowedPunctuation = settingsCache->value("users/allowedpunctuation", "_").toString();
QString disallowedWordsStr = settingsCache->value("users/disallowedwords", "").toString();
QStringList disallowedWords = disallowedWordsStr.split(",", Qt::SkipEmptyParts);
disallowedWords.removeDuplicates();
QVariant displayDisallowedWords = settingsCache->value("users/displaydisallowedwords");
QString disallowedRegExpStr;
if (displayDisallowedWords.isValid()) {
disallowedWordsStr = displayDisallowedWords.toString().trimmed();
if (!disallowedWordsStr.isEmpty()) {
disallowedWordsStr.prepend("\n");
}
} else {
disallowedRegExpStr = settingsCache->value("users/disallowedregexp", "").toString();
}
error = QString("%1|%2|%3|%4|%5|%6|%7|%8|%9")
.arg(minNameLength)
.arg(maxNameLength)
.arg(allowLowercase)
.arg(allowUppercase)
.arg(allowNumerics)
.arg(allowPunctuationPrefix)
.arg(allowedPunctuation)
.arg(disallowedWordsStr)
.arg(disallowedRegExpStr);
if (user.length() < minNameLength || user.length() > maxNameLength)
return false;
if (!allowPunctuationPrefix && allowedPunctuation.contains(user.at(0)))
return false;
for (const QString &word : disallowedWords) {
if (user.contains(word, Qt::CaseInsensitive))
return false;
}
for (const QRegularExpression &regExp : settingsCache->disallowedRegExp) {
if (regExp.match(user).hasMatch())
return false;
}
QString regEx("\\A[");
if (allowLowercase)
regEx.append("a-z");
if (allowUppercase)
regEx.append("A-Z");
if (allowNumerics)
regEx.append("0-9");
regEx.append(QRegularExpression::escape(allowedPunctuation));
regEx.append("]+\\z");
QRegularExpression re = QRegularExpression(regEx);
return re.match(user).hasMatch();
}
bool Servatrice_DatabaseInterface::registerUser(const QString &userName,
const QString &realName,
const QString &password,
bool passwordNeedsHash,
const QString &emailAddress,
const QString &country,
bool active)
{
if (!checkSql())
return false;
QString passwordSha512;
if (passwordNeedsHash) {
passwordSha512 = PasswordHasher::computeHash(password, PasswordHasher::generateRandomSalt());
} else {
passwordSha512 = password;
}
QString token = active ? QString() : PasswordHasher::generateActivationToken();
QSqlQuery *query =
prepareQuery("insert into {prefix}_users "
"(name, realname, password_sha512, email, country, registrationDate, active, token, "
"admin, avatar_bmp, clientid, privlevel, privlevelStartDate, privlevelEndDate) "
"values "
"(:userName, :realName, :password_sha512, :email, :country, UTC_TIMESTAMP(), :active, "
":token, 0, '', '', 'NONE', UTC_TIMESTAMP(), UTC_TIMESTAMP())");
query->bindValue(":userName", userName);
query->bindValue(":realName", realName);
query->bindValue(":password_sha512", passwordSha512);
query->bindValue(":email", emailAddress);
query->bindValue(":country", country);
query->bindValue(":active", active ? 1 : 0);
query->bindValue(":token", token);
if (!execSqlQuery(query)) {
qCWarning(DatabaseInterfaceLog) << "Failed to insert user: " << query->lastError()
<< " sql: " << query->lastQuery();
return false;
}
return true;
}
bool Servatrice_DatabaseInterface::activateUser(const QString &userName, const QString &token)
{
if (!checkSql())
return false;
QSqlQuery *activateQuery =
prepareQuery("select name from {prefix}_users where active=0 and name=:username and token=:token");
activateQuery->bindValue(":username", userName);
activateQuery->bindValue(":token", token);
if (!execSqlQuery(activateQuery)) {
qCWarning(DatabaseInterfaceLog) << "Account activation failed: SQL error." << activateQuery->lastError()
<< " sql: " << activateQuery->lastQuery();
return false;
}
if (activateQuery->next()) {
const QString name = activateQuery->value(0).toString();
// redundant check
if (name == userName) {
QSqlQuery *query = prepareQuery("update {prefix}_users set active=1 where name = :userName");
query->bindValue(":userName", userName);
if (!execSqlQuery(query)) {
qCWarning(DatabaseInterfaceLog)
<< "Failed to activate user: " << query->lastError() << " sql: " << query->lastQuery();
return false;
}
return true;
}
}
return false;
}
AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_ProtocolHandler *handler,
const QString &user,
const QString &password,
const QString &clientId,
QString &reasonStr,
int &banSecondsLeft,
bool passwordNeedsHash)
{
switch (server->getAuthenticationMethod()) {
case Servatrice::AuthenticationNone:
return UnknownUser;
case Servatrice::AuthenticationPassword: {
QString configPassword = settingsCache->value("authentication/password").toString();
if (configPassword == password)
return PasswordRight;
return NotLoggedIn;
}
case Servatrice::AuthenticationSql: {
if (!checkSql())
return UnknownUser;
if (!usernameIsValid(user, reasonStr))
return UsernameInvalid;
if (checkUserIsBanned(handler->getAddress(), user, clientId, reasonStr, banSecondsLeft))
return UserIsBanned;
QSqlQuery *passwordQuery =
prepareQuery("select password_sha512, active from {prefix}_users where name = :name");
passwordQuery->bindValue(":name", user);
if (!execSqlQuery(passwordQuery)) {
qCWarning(DatabaseInterfaceLog) << "Login denied: SQL error";
return NotLoggedIn;
}
if (passwordQuery->next()) {
const QString correctPasswordSha512 = passwordQuery->value(0).toString();
const bool userIsActive = passwordQuery->value(1).toBool();
if (!userIsActive) {
qCWarning(DatabaseInterfaceLog) << "Login denied: user not active";
return UserIsInactive;
}
QString hashedPassword;
if (passwordNeedsHash) {
hashedPassword = PasswordHasher::computeHash(password, correctPasswordSha512.left(16));
} else {
hashedPassword = password;
}
if (correctPasswordSha512 == hashedPassword) {
qCDebug(DatabaseInterfaceLog) << "Login accepted: password right";
return PasswordRight;
} else {
qCDebug(DatabaseInterfaceLog) << "Login denied: password wrong";
return NotLoggedIn;
}
} else {
qCDebug(DatabaseInterfaceLog) << "Login accepted: unknown user";
return UnknownUser;
}
}
}
return UnknownUser;
}
bool Servatrice_DatabaseInterface::checkUserIsBanned(const QString &ipAddress,
const QString &userName,
const QString &clientId,
QString &banReason,
int &banSecondsRemaining)
{
if (server->getAuthenticationMethod() != Servatrice::AuthenticationSql)
return false;
if (!checkSql()) {
qCWarning(DatabaseInterfaceLog) << "Failed to check if user is banned. Database invalid.";
return false;
}
return checkUserIsIpBanned(ipAddress, banReason, banSecondsRemaining) ||
checkUserIsNameBanned(userName, banReason, banSecondsRemaining) ||
checkUserIsIdBanned(clientId, banReason, banSecondsRemaining);
}
bool Servatrice_DatabaseInterface::checkUserIsIdBanned(const QString &clientId,
QString &banReason,
int &banSecondsRemaining)
{
if (clientId.isEmpty())
return false;
QSqlQuery *idBanQuery =
prepareQuery("select"
" timestampdiff(second, now(), date_add(b.time_from, interval b.minutes minute)),"
" b.minutes <=> 0,"
" b.visible_reason"
" from {prefix}_bans b"
" where"
" b.time_from = (select max(c.time_from)"
" from {prefix}_bans c"
" where c.clientid = :id)"
" and b.clientid = :id2");
idBanQuery->bindValue(":id", clientId);
idBanQuery->bindValue(":id2", clientId);
if (!execSqlQuery(idBanQuery)) {
qCWarning(DatabaseInterfaceLog) << "Id ban check failed: SQL error." << idBanQuery->lastError();
return false;
}
if (idBanQuery->next()) {
const int secondsLeft = idBanQuery->value(0).toInt();
const bool permanentBan = idBanQuery->value(1).toInt();
if ((secondsLeft > 0) || permanentBan) {
banReason = idBanQuery->value(2).toString();
banSecondsRemaining = permanentBan ? 0 : secondsLeft;
qCDebug(DatabaseInterfaceLog) << "User is banned by client id" << clientId;
return true;
}
}
return false;
}
bool Servatrice_DatabaseInterface::checkUserIsNameBanned(const QString &userName,
QString &banReason,
int &banSecondsRemaining)
{
QSqlQuery *nameBanQuery =
prepareQuery("select timestampdiff(second, now(), date_add(b.time_from, interval b.minutes minute)), b.minutes "
"<=> 0, b.visible_reason from {prefix}_bans b where b.time_from = (select max(c.time_from) from "
"{prefix}_bans c where c.user_name = :name2) and b.user_name = :name1");
nameBanQuery->bindValue(":name1", userName);
nameBanQuery->bindValue(":name2", userName);
if (!execSqlQuery(nameBanQuery)) {
qCWarning(DatabaseInterfaceLog) << "Name ban check failed: SQL error" << nameBanQuery->lastError();
return false;
}
if (nameBanQuery->next()) {
const int secondsLeft = nameBanQuery->value(0).toInt();
const bool permanentBan = nameBanQuery->value(1).toInt();
if ((secondsLeft > 0) || permanentBan) {
banReason = nameBanQuery->value(2).toString();
banSecondsRemaining = permanentBan ? 0 : secondsLeft;
qCDebug(DatabaseInterfaceLog) << "Username" << userName << "is banned by name";
return true;
}
}
return false;
}
bool Servatrice_DatabaseInterface::checkUserIsIpBanned(const QString &ipAddress,
QString &banReason,
int &banSecondsRemaining)
{
QSqlQuery *ipBanQuery =
prepareQuery("select"
" timestampdiff(second, now(), date_add(b.time_from, interval b.minutes minute)),"
" b.minutes <=> 0,"
" b.visible_reason"
" from {prefix}_bans b"
" where"
" b.time_from = (select max(c.time_from)"
" from {prefix}_bans c"
" where c.ip_address = :address)"
" and b.ip_address = :address2");
ipBanQuery->bindValue(":address", ipAddress);
ipBanQuery->bindValue(":address2", ipAddress);
if (!execSqlQuery(ipBanQuery)) {
qCWarning(DatabaseInterfaceLog) << "IP ban check failed: SQL error." << ipBanQuery->lastError();
return false;
}
if (ipBanQuery->next()) {
const int secondsLeft = ipBanQuery->value(0).toInt();
const bool permanentBan = ipBanQuery->value(1).toInt();
if ((secondsLeft > 0) || permanentBan) {
banReason = ipBanQuery->value(2).toString();
banSecondsRemaining = permanentBan ? 0 : secondsLeft;
qCDebug(DatabaseInterfaceLog) << "User is banned by address" << ipAddress;
return true;
}
}
return false;
}
bool Servatrice_DatabaseInterface::activeUserExists(const QString &user)
{
if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) {
checkSql();
QSqlQuery *query = prepareQuery("select 1 from {prefix}_users where name = :name and active = 1");
query->bindValue(":name", user);
if (!execSqlQuery(query))
return false;
return query->next();
}
return false;
}
bool Servatrice_DatabaseInterface::userExists(const QString &user)
{
if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) {
checkSql();
QSqlQuery *query = prepareQuery("select 1 from {prefix}_users where name = :name");
query->bindValue(":name", user);
if (!execSqlQuery(query))
return false;
return query->next();
}
return false;
}
QString Servatrice_DatabaseInterface::getUserSalt(const QString &user)
{
if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) {
checkSql();
QSqlQuery *query =
prepareQuery("SELECT SUBSTRING(password_sha512, 1, 16) FROM {prefix}_users WHERE name = :name");
query->bindValue(":name", user);
if (!execSqlQuery(query)) {
return {};
}
if (!query->next()) {
return {};
}
return query->value(0).toString();
}
return {};
}
int Servatrice_DatabaseInterface::getUserIdInDB(const QString &name)
{
if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) {
QSqlQuery *query = prepareQuery("select id from {prefix}_users where name = :name and active = 1");
query->bindValue(":name", name);
if (!execSqlQuery(query))
return -1;
if (!query->next())
return -1;
return query->value(0).toInt();
}
return -1;
}
bool Servatrice_DatabaseInterface::isInBuddyList(const QString &whoseList, const QString &who)
{
if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone)
return false;
if (!checkSql())
return false;
int id1 = getUserIdInDB(whoseList);
int id2 = getUserIdInDB(who);
QSqlQuery *query =
prepareQuery("select 1 from {prefix}_buddylist where id_user1 = :id_user1 and id_user2 = :id_user2");
query->bindValue(":id_user1", id1);
query->bindValue(":id_user2", id2);
if (!execSqlQuery(query))
return false;
return query->next();
}
bool Servatrice_DatabaseInterface::isInIgnoreList(const QString &whoseList, const QString &who)
{
if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone)
return false;
if (!checkSql())
return false;
int id1 = getUserIdInDB(whoseList);
int id2 = getUserIdInDB(who);
QSqlQuery *query =
prepareQuery("select 1 from {prefix}_ignorelist where id_user1 = :id_user1 and id_user2 = :id_user2");
query->bindValue(":id_user1", id1);
query->bindValue(":id_user2", id2);
if (!execSqlQuery(query))
return false;
return query->next();
}
ServerInfo_User Servatrice_DatabaseInterface::evalUserQueryResult(const QSqlQuery *query, bool complete, bool withId)
{
ServerInfo_User result;
if (withId)
result.set_id(query->value(0).toInt());
result.set_name(query->value(1).toString().toStdString());
const int is_admin = query->value(2).toInt();
int userLevel = ServerInfo_User::IsUser | ServerInfo_User::IsRegistered;
if (is_admin & 1)
userLevel |= ServerInfo_User::IsAdmin | ServerInfo_User::IsModerator;
else if (is_admin & 2)
userLevel |= ServerInfo_User::IsModerator;
if (is_admin & 4)
userLevel |= ServerInfo_User::IsJudge;
result.set_user_level(userLevel);
const QString country = query->value(3).toString();
if (!country.isEmpty())
result.set_country(country.toStdString());
const QString privlevel = query->value(4).toString();
if (!privlevel.isEmpty())
result.set_privlevel(privlevel.toStdString());
const auto &pawn_left_override = query->value(5).toString();
const auto &pawn_right_override = query->value(6).toString();
if (!pawn_left_override.isEmpty()) {
result.mutable_pawn_colors()->set_left_side(pawn_left_override.toStdString());
}
if (!pawn_right_override.isEmpty()) {
result.mutable_pawn_colors()->set_right_side(pawn_right_override.toStdString());
}
if (complete) {
const QString realName = query->value(7).toString();
if (!realName.isEmpty())
result.set_real_name(realName.toStdString());
const QByteArray avatarBmp = query->value(8).toByteArray();
if (avatarBmp.size())
result.set_avatar_bmp(avatarBmp.data(), avatarBmp.size());
const QDateTime regDate = query->value(9).toDateTime();
if (!regDate.toString(Qt::ISODate).isEmpty()) {
// the registration date is in utc
qint64 accountAgeInSeconds = regDate.secsTo(QDateTime::currentDateTimeUtc());
result.set_accountage_secs(accountAgeInSeconds);
}
const QString email = query->value(10).toString();
if (!email.isEmpty())
result.set_email(email.toStdString());
const QString clientid = query->value(11).toString();
if (!clientid.isEmpty())
result.set_clientid(clientid.toStdString());
}
return result;
}
ServerInfo_User Servatrice_DatabaseInterface::getUserData(const QString &name, bool withId)
{
ServerInfo_User result;
result.set_name(name.toStdString());
result.set_user_level(ServerInfo_User::IsUser);
if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) {
if (!checkSql())
return result;
QSqlQuery *query = prepareQuery("select id, name, admin, country, privlevel, leftPawnColorOverride, "
"rightPawnColorOverride, realname, avatar_bmp, registrationDate, "
"email, clientid from {prefix}_users where "
"name = :name and active = 1");
query->bindValue(":name", name);
if (!execSqlQuery(query))
return result;
if (query->next())
return evalUserQueryResult(query, true, withId);
else
return result;
} else
return result;
}
void Servatrice_DatabaseInterface::clearSessionTables()
{
lockSessionTables();
QSqlQuery *query =
prepareQuery("update {prefix}_sessions set end_time=now() where end_time is null and id_server = :id_server");
query->bindValue(":id_server", server->getServerID());
execSqlQuery(query);
unlockSessionTables();
}
void Servatrice_DatabaseInterface::lockSessionTables()
{
QSqlQuery *query = prepareQuery("lock tables {prefix}_sessions write, {prefix}_users read");
execSqlQuery(query);
}
void Servatrice_DatabaseInterface::unlockSessionTables()
{
QSqlQuery *query = prepareQuery("unlock tables");
execSqlQuery(query);
}
bool Servatrice_DatabaseInterface::userSessionExists(const QString &userName)
{
// Call only after lockSessionTables().
QSqlQuery *query = prepareQuery(
"select 1 from {prefix}_sessions where user_name = :user_name and id_server = :id_server and end_time is null");
query->bindValue(":id_server", server->getServerID());
query->bindValue(":user_name", userName);
if (!execSqlQuery(query)) {
return false;
};
return query->next();
}
qint64 Servatrice_DatabaseInterface::startSession(const QString &userName,
const QString &address,
const QString &clientId,
const QString &connectionType)
{
if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone)
return -1;
if (!checkSql())
return -1;
QSqlQuery *query = prepareQuery("insert into {prefix}_sessions (user_name, id_server, ip_address, start_time, "
"clientid, connection_type) values(:user_name, :id_server, :ip_address, NOW(), "
":client_id, :connection_type)");
query->bindValue(":user_name", userName);
query->bindValue(":id_server", server->getServerID());
query->bindValue(":ip_address", address);
query->bindValue(":client_id", clientId);
query->bindValue(":connection_type", connectionType);
if (execSqlQuery(query))
return query->lastInsertId().toInt();
return -1;
}
void Servatrice_DatabaseInterface::endSession(qint64 sessionId)
{
if (server->getAuthenticationMethod() == Servatrice::AuthenticationNone)
return;
if (!checkSql())
return;
auto *query = prepareQuery("update {prefix}_sessions set end_time=NOW() where id = :id_session");
query->bindValue(":id_session", sessionId);
execSqlQuery(query);
}
QMap<QString, ServerInfo_User> Servatrice_DatabaseInterface::getBuddyList(const QString &name)
{
QMap<QString, ServerInfo_User> result;
if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) {
checkSql();
QSqlQuery *query = prepareQuery("select a.id, a.name, a.admin, a.country, a.privlevel, "
"a.leftPawnColorOverride, a.rightPawnColorOverride from {prefix}_users a "
"left join {prefix}_buddylist b on a.id = b.id_user2 left join {prefix}_users "
"c on b.id_user1 = c.id where c.name = :name");
query->bindValue(":name", name);
if (!execSqlQuery(query))
return result;
while (query->next()) {
const ServerInfo_User &temp = evalUserQueryResult(query, false);
result.insert(QString::fromStdString(temp.name()), temp);
}
}
return result;
}
QMap<QString, ServerInfo_User> Servatrice_DatabaseInterface::getIgnoreList(const QString &name)
{
QMap<QString, ServerInfo_User> result;
if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) {
checkSql();
QSqlQuery *query = prepareQuery("select a.id, a.name, a.admin, a.country, a.privlevel, "
"a.leftPawnColorOverride, a.rightPawnColorOverride from {prefix}_users a "
"left join {prefix}_ignorelist b on a.id = b.id_user2 left join {prefix}_users "
"c on b.id_user1 = c.id where c.name = :name");
query->bindValue(":name", name);
if (!execSqlQuery(query))
return result;
while (query->next()) {
ServerInfo_User temp = evalUserQueryResult(query, false);
result.insert(QString::fromStdString(temp.name()), temp);
}
}
return result;
}
int Servatrice_DatabaseInterface::getNextGameId()
{
if (!sqlDatabase.isValid())
return server->getNextLocalGameId();
if (!checkSql())
return -1;
QSqlQuery *query = prepareQuery("insert into {prefix}_games (time_started) values (now())");
if (!execSqlQuery(query)) {
return -1;
}
return query->lastInsertId().toInt();
}
int Servatrice_DatabaseInterface::getNextReplayId()
{
if (!checkSql())
return -1;
QSqlQuery *query = prepareQuery("insert into {prefix}_replays (id_game) values (NULL)");
if (!execSqlQuery(query)) {
return -1;
}
return query->lastInsertId().toInt();
}
void Servatrice_DatabaseInterface::storeGameInformation(const QString &roomName,
const QStringList &roomGameTypes,
const ServerInfo_Game &gameInfo,
const QSet<QString> &allPlayersEver,
const QSet<QString> &allSpectatorsEver,
const QList<GameReplay *> &replayList)
{
if (!checkSql())
return;
if (!settingsCache->value("game/store_replays", 1).toBool())
return;
QVariantList gameIds1, playerNames, gameIds2, userIds, replayNames;
QSetIterator<QString> playerIterator(allPlayersEver);
while (playerIterator.hasNext()) {
gameIds1.append(gameInfo.game_id());
const QString &playerName = playerIterator.next();
playerNames.append(playerName);
}
QSet<QString> allUsersInGame = allPlayersEver + allSpectatorsEver;
QSetIterator<QString> allUsersIterator(allUsersInGame);
while (allUsersIterator.hasNext()) {
int id = getUserIdInDB(allUsersIterator.next());
if (id == -1)
continue;
gameIds2.append(gameInfo.game_id());
userIds.append(id);
replayNames.append(QString::fromStdString(gameInfo.description()));
}
QVariantList replayIds, replayGameIds, replayDurations, replayBlobs;
for (int i = 0; i < replayList.size(); ++i) {
QByteArray blob;
#if GOOGLE_PROTOBUF_VERSION > 3001000
const unsigned int size = static_cast<unsigned int>(replayList[i]->ByteSizeLong());
#else
const unsigned int size = static_cast<unsigned int>(replayList[i]->ByteSize());
#endif
blob.resize(size);
qulonglong replayId = replayList[i]->replay_id();
if (replayList[i]->SerializeToArray(blob.data(), size)) {
replayIds.append(QVariant(replayId));
replayGameIds.append(gameInfo.game_id());
replayDurations.append(replayList[i]->duration_seconds());
replayBlobs.append(blob);
} else {
qCWarning(DatabaseInterfaceLog)
<< "failed to serialise replay, id:" << replayId << "game:" << gameInfo.game_id();
}
}
{
QSqlQuery *query = prepareQuery("update {prefix}_games set room_name=:room_name, descr=:descr, "
"creator_name=:creator_name, password=:password, game_types=:game_types, "
"player_count=:player_count, time_finished=now() where id=:id_game");
query->bindValue(":room_name", roomName);
query->bindValue(":id_game", gameInfo.game_id());
query->bindValue(":descr", QString::fromStdString(gameInfo.description()));
query->bindValue(":creator_name", QString::fromStdString(gameInfo.creator_info().name()));
query->bindValue(":password", gameInfo.with_password() ? 1 : 0);
query->bindValue(":game_types", roomGameTypes.isEmpty() ? QString("") : roomGameTypes.join(", "));
query->bindValue(":player_count", gameInfo.max_players());
if (!execSqlQuery(query))
return;
}
{
QSqlQuery *query =
prepareQuery("insert into {prefix}_games_players (id_game, player_name) values (:id_game, :player_name)");
query->bindValue(":id_game", gameIds1);
query->bindValue(":player_name", playerNames);
query->execBatch();
}
{
QSqlQuery *query = prepareQuery(
"update {prefix}_replays set id_game=:id_game, duration=:duration, replay=:replay where id=:id_replay");
query->bindValue(":id_replay", replayIds);
query->bindValue(":id_game", replayGameIds);
query->bindValue(":duration", replayDurations);
query->bindValue(":replay", replayBlobs);
query->execBatch();
}
{
QSqlQuery *query = prepareQuery("insert into {prefix}_replays_access (id_game, id_player, replay_name) values "
"(:id_game, :id_player, :replay_name)");
query->bindValue(":id_game", gameIds2);
query->bindValue(":id_player", userIds);
query->bindValue(":replay_name", replayNames);
query->execBatch();
}
}
DeckList *Servatrice_DatabaseInterface::getDeckFromDatabase(int deckId, int userId)
{
checkSql();
QSqlQuery *query =
prepareQuery("select content from {prefix}_decklist_files where id = :id and id_user = :id_user");
query->bindValue(":id", deckId);
query->bindValue(":id_user", userId);
execSqlQuery(query);
if (!query->next())
throw Response::RespNameNotFound;
DeckList *deck = new DeckList;
deck->loadFromString_Native(query->value(0).toString());
return deck;
}
void Servatrice_DatabaseInterface::logMessage(const int senderId,
const QString &senderName,
const QString &senderIp,
const QString &logMessage,
LogMessage_TargetType targetType,
const int targetId,
const QString &targetName)
{
QString targetTypeString;
switch (targetType) {
case MessageTargetRoom:
if (!settingsCache->value("logging/log_user_msg_room", 0).toBool())
return;
targetTypeString = "room";
break;
case MessageTargetGame:
if (!settingsCache->value("logging/log_user_msg_game", 0).toBool())
return;
targetTypeString = "game";
break;
case MessageTargetChat:
if (!settingsCache->value("logging/log_user_msg_chat", 0).toBool())
return;
targetTypeString = "chat";
break;
case MessageTargetIslRoom:
if (!settingsCache->value("logging/log_user_msg_isl", 0).toBool())
return;
targetTypeString = "room";
break;
default:
return;
}
QSqlQuery *query = prepareQuery("insert into {prefix}_log (log_time, sender_id, sender_name, sender_ip, "
"log_message, target_type, target_id, target_name) values (now(), :sender_id, "
":sender_name, :sender_ip, :log_message, :target_type, :target_id, :target_name)");
query->bindValue(":sender_id", senderId < 1 ? QVariant() : senderId);
query->bindValue(":sender_name", senderName);
query->bindValue(":sender_ip", senderIp);
query->bindValue(":log_message", logMessage);
query->bindValue(":target_type", targetTypeString);
query->bindValue(":target_id", (targetType == MessageTargetChat && targetId < 1) ? QVariant() : targetId);
query->bindValue(":target_name", targetName);
execSqlQuery(query);
}
bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user,
const QString &password,
bool passwordNeedsHash)
{
QString passwordSha512 = password;
if (passwordNeedsHash) {
passwordSha512 = PasswordHasher::computeHash(password, PasswordHasher::generateRandomSalt());
}
QSqlQuery *passwordQuery = prepareQuery("update {prefix}_users set password_sha512=:password, "
"passwordLastChangedDate = NOW() where name = :name");
passwordQuery->bindValue(":password", passwordSha512);
passwordQuery->bindValue(":name", user);
if (execSqlQuery(passwordQuery))
return true;
return false;
}
bool Servatrice_DatabaseInterface::changeUserPassword(const QString &user,
const QString &oldPassword,
bool oldPasswordNeedsHash,
const QString &newPassword,
bool newPasswordNeedsHash)
{
if (server->getAuthenticationMethod() != Servatrice::AuthenticationSql)
return false;
if (!checkSql())
return false;
QString error;
if (!usernameIsValid(user, error))
return false;
QSqlQuery *passwordQuery = prepareQuery("select password_sha512 from {prefix}_users where name = :name");
passwordQuery->bindValue(":name", user);
if (!execSqlQuery(passwordQuery)) {
qCWarning(DatabaseInterfaceLog) << "Change password denied: SQL error";
return false;
}
if (!passwordQuery->next())
return false;
const QString correctPasswordSha512 = passwordQuery->value(0).toString();
QString oldPasswordSha512 = oldPassword;
if (oldPasswordNeedsHash) {
QString salt = correctPasswordSha512.left(16);
oldPasswordSha512 = PasswordHasher::computeHash(oldPassword, salt);
}
if (correctPasswordSha512 != oldPasswordSha512)
return false;
return changeUserPassword(user, newPassword, newPasswordNeedsHash);
}
int Servatrice_DatabaseInterface::getActiveUserCount(QString connectionType)
{
int userCount = 0;
if (!checkSql())
return userCount;
QString text = "select count(*) from {prefix}_sessions where id_server = :serverid AND end_time is NULL";
if (!connectionType.isEmpty())
text += " AND connection_type = :connection_type";
QSqlQuery *query = prepareQuery(text);
query->bindValue(":serverid", server->getServerID());
if (!connectionType.isEmpty())
query->bindValue(":connection_type", connectionType);
if (!execSqlQuery(query))
return userCount;
if (query->next())
userCount = query->value(0).toInt();
return userCount;
}
void Servatrice_DatabaseInterface::updateUsersClientID(const QString &userName, const QString &userClientID)
{
if (!checkSql())
return;
QSqlQuery *query = prepareQuery("update {prefix}_users set clientid = :clientid where name = :username");
query->bindValue(":clientid", userClientID);
query->bindValue(":username", userName);
execSqlQuery(query);
}
void Servatrice_DatabaseInterface::updateUsersLastLoginData(const QString &userName, const QString &clientVersion)
{
if (!checkSql())
return;
int usersID = 0;
QSqlQuery *query = prepareQuery("select id from {prefix}_users where name = :user_name");
query->bindValue(":user_name", userName);
if (!execSqlQuery(query)) {
qCWarning(DatabaseInterfaceLog) << "Failed to locate user id when updating users last login data: SQL Error";
return;
}
if (query->next()) {
usersID = query->value(0).toInt();
}
if (usersID) {
int userCount = 0;
query = prepareQuery("select count(id) from {prefix}_user_analytics where id = :user_id");
query->bindValue(":user_id", usersID);
if (!execSqlQuery(query))
return;
if (query->next()) {
userCount = query->value(0).toInt();
}
if (!userCount) {
query = prepareQuery(
"insert into {prefix}_user_analytics (id,client_ver,last_login) values (:user_id,:client_ver,NOW())");
query->bindValue(":user_id", usersID);
query->bindValue(":client_ver", clientVersion);
execSqlQuery(query);
} else {
query = prepareQuery(
"update {prefix}_user_analytics set last_login = NOW(), client_ver = :client_ver where id = :user_id");
query->bindValue(":client_ver", clientVersion);
query->bindValue(":user_id", usersID);
execSqlQuery(query);
}
}
}
QList<ServerInfo_Ban> Servatrice_DatabaseInterface::getUserBanHistory(const QString userName)
{
QList<ServerInfo_Ban> results;
ServerInfo_Ban banDetails;
if (!checkSql())
return results;
QSqlQuery *query =
prepareQuery("SELECT A.id_admin, A.time_from, A.minutes, A.reason, A.visible_reason, B.name AS name_admin FROM "
"{prefix}_bans A LEFT JOIN {prefix}_users B ON A.id_admin=B.id WHERE A.user_name = :user_name");
query->bindValue(":user_name", userName);
if (!execSqlQuery(query)) {
qCWarning(DatabaseInterfaceLog) << "Failed to collect ban history information: SQL Error";
return results;
}
while (query->next()) {
banDetails.set_admin_id(QString(query->value(0).toString()).toStdString());
banDetails.set_admin_name(QString(query->value(5).toString()).toStdString());
banDetails.set_ban_time(QString(query->value(1).toString()).toStdString());
banDetails.set_ban_length(QString(query->value(2).toString()).toStdString());
banDetails.set_ban_reason(QString(query->value(3).toString()).toStdString());
banDetails.set_visible_reason(QString(query->value(4).toString()).toStdString());
results << banDetails;
}
return results;
}
bool Servatrice_DatabaseInterface::addWarning(const QString userName,
const QString adminName,
const QString warningReason,
const QString clientID)
{
if (!checkSql())
return false;
int userID = getUserIdInDB(userName);
QSqlQuery *query =
prepareQuery("insert into {prefix}_warnings (user_id,user_name,mod_name,reason,time_of,clientid) values "
"(:user_id,:user_name,:mod_name,:warn_reason,NOW(),:client_id)");
query->bindValue(":user_id", userID);
query->bindValue(":user_name", userName);
query->bindValue(":mod_name", adminName);
query->bindValue(":warn_reason", warningReason);
query->bindValue(":client_id", clientID);
if (!execSqlQuery(query)) {
qCWarning(DatabaseInterfaceLog) << "Failed to collect create warning history information: SQL Error";
return false;
}
return true;
}
QList<ServerInfo_Warning> Servatrice_DatabaseInterface::getUserWarnHistory(const QString userName)
{
QList<ServerInfo_Warning> results;
ServerInfo_Warning warnDetails;
if (!checkSql())
return results;
int userID = getUserIdInDB(userName);
QSqlQuery *query =
prepareQuery("SELECT user_name, mod_name, reason, time_of FROM {prefix}_warnings WHERE user_id = :user_id");
query->bindValue(":user_id", userID);
if (!execSqlQuery(query)) {
qCWarning(DatabaseInterfaceLog) << "Failed to collect warning history information: SQL Error";
return results;
}
while (query->next()) {
warnDetails.set_user_name(QString(query->value(0).toString()).toStdString());
warnDetails.set_admin_name(QString(query->value(1).toString()).toStdString());
warnDetails.set_reason(QString(query->value(2).toString()).toStdString());
warnDetails.set_time_of(QString(query->value(3).toString()).toStdString());
results << warnDetails;
}
return results;
}
QList<ServerInfo_ChatMessage> Servatrice_DatabaseInterface::getMessageLogHistory(const QString &user,
const QString &ipaddress,
const QString &gamename,
const QString &gameid,
const QString &message,
bool &chat,
bool &game,
bool &room,
int &range,
int &maxresults)
{
QList<ServerInfo_ChatMessage> results;
ServerInfo_ChatMessage chatMessage;
if (!checkSql())
return results;
if (user.isEmpty() && ipaddress.isEmpty() && gameid.isEmpty() && gamename.isEmpty()) {
// To ensure quick results and minimal lag, require an indexed field
return results;
}
// BUILD QUERY STRING BASED ON PASSED IN VALUES
QString queryString = "SELECT * FROM {prefix}_log WHERE `sender_ip` IS NOT NULL";
if (!user.isEmpty())
queryString.append(" AND (`sender_name` = :user_name OR `target_name` = :user_name)");
if (!ipaddress.isEmpty())
queryString.append(" AND `sender_ip` = :ip_to_find");
if (!gameid.isEmpty())
queryString.append(" AND (`target_id` = :game_id AND `target_type` = 'game')");
if (!gamename.isEmpty())
queryString.append(" AND (`target_name` = :game_name AND `target_type` = 'game')");
if (!message.isEmpty())
queryString.append(" AND `log_message` LIKE :log_message");
if (chat || game || room) {
queryString.append(" AND (");
if (chat)
queryString.append("`target_type` = 'chat'");
if (game) {
if (chat)
queryString.append(" OR `target_type` = 'game'");
else
queryString.append("`target_type` = 'game'");
}
if (room) {
if (game || chat)
queryString.append(" OR `target_type` = 'room'");
else
queryString.append("`target_type` = 'room'");
}
queryString.append(")");
}
if (range)
queryString.append(" AND log_time >= DATE_SUB(now(), INTERVAL :range_time HOUR)");
if (maxresults)
queryString.append(" LIMIT :limit_size");
QSqlQuery *query = prepareQuery(queryString);
if (!user.isEmpty()) {
query->bindValue(":user_name", user);
}
if (!ipaddress.isEmpty()) {
query->bindValue(":ip_to_find", ipaddress);
}
if (!gameid.isEmpty()) {
query->bindValue(":game_id", gameid);
}
if (!gamename.isEmpty()) {
query->bindValue(":game_name", gamename);
}
if (!message.isEmpty()) {
query->bindValue(":log_message", message);
}
if (range) {
query->bindValue(":range_time", range);
}
if (maxresults) {
query->bindValue(":limit_size", maxresults);
}
if (!execSqlQuery(query)) {
qCWarning(DatabaseInterfaceLog) << "Failed to collect log history information: SQL Error";
return results;
}
while (query->next()) {
chatMessage.set_time(QString(query->value(0).toString()).toStdString());
chatMessage.set_sender_id(QString(query->value(1).toString()).toStdString());
chatMessage.set_sender_name(QString(query->value(2).toString()).toStdString());
chatMessage.set_sender_ip(QString(query->value(3).toString()).toStdString());
chatMessage.set_message(QString(query->value(4).toString()).toStdString());
chatMessage.set_target_type(QString(query->value(5).toString()).toStdString());
chatMessage.set_target_id(QString(query->value(6).toString()).toStdString());
chatMessage.set_target_name(QString(query->value(7).toString()).toStdString());
results << chatMessage;
}
return results;
}
int Servatrice_DatabaseInterface::checkNumberOfUserAccounts(const QString &email)
{
if (!checkSql())
return 0;
QSqlQuery *query = prepareQuery("SELECT count(email) FROM {prefix}_users WHERE email = :user_email");
query->bindValue(":user_email", email);
if (!execSqlQuery(query)) {
qCWarning(DatabaseInterfaceLog)
<< "Failed to identify the number of users accounts for users email address: SQL Error";
return 0;
}
if (query->next())
return query->value(0).toInt();
return 0;
}
bool Servatrice_DatabaseInterface::addForgotPassword(const QString &user)
{
if (!checkSql())
return false;
if (!updateUserToken(PasswordHasher::generateActivationToken(), user))
return false;
QSqlQuery *query = prepareQuery("insert into {prefix}_forgot_password (name,requestDate) values (:username,NOW())");
query->bindValue(":username", user);
if (execSqlQuery(query))
return true;
return false;
}
bool Servatrice_DatabaseInterface::removeForgotPassword(const QString &user)
{
if (!checkSql())
return false;
QSqlQuery *query = prepareQuery("delete from {prefix}_forgot_password where name = :username");
query->bindValue(":username", user);
if (execSqlQuery(query))
return true;
return false;
}
bool Servatrice_DatabaseInterface::doesForgotPasswordExist(const QString &user)
{
if (!checkSql())
return false;
QSqlQuery *query = prepareQuery("select count(name) from {prefix}_forgot_password where name = :user_name AND "
"requestDate > (now() - interval :minutes minute)");
query->bindValue(":user_name", user);
query->bindValue(":minutes", QString::number(server->getForgotPasswordTokenLife()));
if (!execSqlQuery(query))
return false;
if (query->next())
if (query->value("count(name)").toInt() > 0)
return true;
return false;
}
bool Servatrice_DatabaseInterface::updateUserToken(const QString &token, const QString &user)
{
if (!checkSql())
return false;
if (token.isEmpty() || user.isEmpty())
return false;
QSqlQuery *query = prepareQuery("update {prefix}_users set token = :token where name = :user_name");
query->bindValue(":user_name", user);
query->bindValue(":token", token);
if (execSqlQuery(query))
return true;
return false;
}
bool Servatrice_DatabaseInterface::validateTableColumnStringData(const QString &table,
const QString &column,
const QString &_user,
const QString &_datatocheck)
{
if (!checkSql())
return false;
if (table.isEmpty() || column.isEmpty() || _user.isEmpty() || _datatocheck.isEmpty())
return false;
QString formatedQuery = QString("select %1 from %2 where name = :user_name").arg(column).arg(table);
QSqlQuery *query = prepareQuery(formatedQuery);
query->bindValue(":user_name", _user);
if (!execSqlQuery(query))
return false;
if (query->next())
if (query->value(column).toString().toLower() == _datatocheck.toLower())
return true;
return false;
}
void Servatrice_DatabaseInterface::addAuditRecord(const QString &user,
const QString &ipaddress,
const QString &clientid,
const QString &action,
const QString &details,
const bool &results = false)
{
if (!checkSql())
return;
if (!server->getEnableAudit())
return;
if (user.isEmpty() || ipaddress.isEmpty() || clientid.isEmpty() || action.isEmpty())
return;
QSqlQuery *query = prepareQuery("insert into {prefix}_audit "
"(id_server,name,ip_address,clientid,incidentDate,action,results,details) values "
"(:idserver,:username,:ipaddress,:clientid,NOW(),:action,:results,:details)");
query->bindValue(":idserver", server->getServerID());
query->bindValue(":username", user);
query->bindValue(":ipaddress", ipaddress);
query->bindValue(":clientid", clientid);
query->bindValue(":action", action);
query->bindValue(":results", results ? "success" : "fail");
query->bindValue(":details", details);
execSqlQuery(query);
}