#include "servatrice_database_interface.h" #include "servatrice.h" #include "serversocketinterface.h" #include "settingscache.h" #include #include #include #include #include #include #include #include #include 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 ®Exp : 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 Servatrice_DatabaseInterface::getBuddyList(const QString &name) { QMap 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 Servatrice_DatabaseInterface::getIgnoreList(const QString &name) { QMap 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 &allPlayersEver, const QSet &allSpectatorsEver, const QList &replayList) { if (!checkSql()) return; if (!settingsCache->value("game/store_replays", 1).toBool()) return; QVariantList gameIds1, playerNames, gameIds2, userIds, replayNames; QSetIterator playerIterator(allPlayersEver); while (playerIterator.hasNext()) { gameIds1.append(gameInfo.game_id()); const QString &playerName = playerIterator.next(); playerNames.append(playerName); } QSet allUsersInGame = allPlayersEver + allSpectatorsEver; QSetIterator 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(replayList[i]->ByteSizeLong()); #else const unsigned int size = static_cast(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 Servatrice_DatabaseInterface::getUserBanHistory(const QString userName) { QList 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 Servatrice_DatabaseInterface::getUserWarnHistory(const QString userName) { QList 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 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 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); }