From 03adbd978749e66232e752d11a23fd8d63e18fc7 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:16:25 +0100 Subject: [PATCH] [Server] Move replay code exchange into the database interface (#7839) createHashForReplay, cmdReplayGetCode, cmdReplaySubmitCode, cmdReplayDownloadByGameId and cmdGrantReplayAccess still issued their own SQL through Servatrice_DatabaseInterface. Add checkReplayAccess, getReplayName, getReplayHashSource, addReplayAccess, getLatestReplayForGame and grantReplayAccessByName to Server_DatabaseInterface, implement them with the same statements, bind order and response mapping, and let the commands ask the injected interface. Hashing and code parsing stay in the handler; the shared insert is reused by both grant paths. --- .../server/remote/server_database_interface.h | 30 ++++ .../src/servatrice_database_interface.cpp | 99 ++++++++++++ .../src/servatrice_database_interface.h | 6 + servatrice/src/serversocketinterface.cpp | 149 ++++++------------ 4 files changed, 181 insertions(+), 103 deletions(-) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h index 04683bbc4..e93318e8f 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h @@ -361,6 +361,36 @@ public: { return ReplayWriteResult::Failed; } + /** @brief Checks whether a user has an access row for a match's replays. */ + virtual ReplayAccessResult checkReplayAccess(int /* gameId */, int /* userId */) + { + return ReplayAccessResult::Failed; + } + /** @brief Reads the stored name of a match's replay access row. */ + virtual ReplayAccessResult getReplayName(int /* gameId */, QString & /* replayName */) + { + return ReplayAccessResult::Failed; + } + /** @brief Copies the leading bytes of a match's replay files, the input of its share code. */ + virtual bool getReplayHashSource(int /* gameId */, QByteArray & /* source */) + { + return false; + } + /** @brief Grants one user access to a match's replays. */ + virtual bool addReplayAccess(int /* gameId */, int /* userId */, const QString & /* replayName */) + { + return false; + } + /** @brief Fetches the newest replay file of a game for a download-by-game request. */ + virtual ReplayAccessResult getLatestReplayForGame(int /* gameId */, int & /* replayId */, QByteArray & /* data */) + { + return ReplayAccessResult::Failed; + } + /** @brief Grants a user, resolved by name, access to an existing match's replays. */ + virtual ReplayWriteResult grantReplayAccessByName(int /* gameId */, const QString & /* userName */) + { + return ReplayWriteResult::Failed; + } /** @brief Append a moderator/admin action to the server audit log. */ virtual void addAuditRecord(const QString & /* user */, const QString & /* ipaddress */, diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index 7fe518f1b..89a586298 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -1939,6 +1939,105 @@ Server_DatabaseInterface::ReplayWriteResult Servatrice_DatabaseInterface::delete return query->numRowsAffected() > 0 ? ReplayWriteResult::Ok : ReplayWriteResult::NotFound; } +Server_DatabaseInterface::ReplayAccessResult Servatrice_DatabaseInterface::checkReplayAccess(int gameId, int userId) +{ + QSqlQuery *query = + prepareQuery("select 1 from {prefix}_replays_access where id_game = :id_game and id_player = :id_player"); + query->bindValue(":id_game", gameId); + query->bindValue(":id_player", userId); + if (!execSqlQuery(query)) { + return ReplayAccessResult::Failed; + } + if (!query->next()) { + return ReplayAccessResult::AccessDenied; + } + return ReplayAccessResult::Ok; +} + +Server_DatabaseInterface::ReplayAccessResult Servatrice_DatabaseInterface::getReplayName(int gameId, + QString &replayName) +{ + QSqlQuery *query = prepareQuery("select replay_name from {prefix}_replays_access where id_game = :id_game limit 1"); + query->bindValue(":id_game", gameId); + if (!execSqlQuery(query)) { + return ReplayAccessResult::Failed; + } + if (!query->next()) { + return ReplayAccessResult::NotFound; + } + replayName = query->value(0).toString(); + return ReplayAccessResult::Ok; +} + +bool Servatrice_DatabaseInterface::getReplayHashSource(int gameId, QByteArray &source) +{ + QSqlQuery *query = prepareQuery("select replay from {prefix}_replays where id_game = :id_game limit 3"); + query->bindValue(":id_game", gameId); + if (!execSqlQuery(query)) { + return false; + } + + while (query->next()) { + QByteArray replay = query->value(0).toByteArray(); + replay.truncate(128); + source.append(replay); + } + return true; +} + +bool Servatrice_DatabaseInterface::addReplayAccess(int gameId, int userId, const QString &replayName) +{ + QSqlQuery *query = + prepareQuery("insert into {prefix}_replays_access (id_game, id_player, replay_name, do_not_hide) " + "values(:idgame, :idplayer, :replayname, 0)"); + query->bindValue(":idgame", gameId); + query->bindValue(":idplayer", userId); + query->bindValue(":replayname", replayName); + + return execSqlQuery(query); +} + +Server_DatabaseInterface::ReplayAccessResult +Servatrice_DatabaseInterface::getLatestReplayForGame(int gameId, int &replayId, QByteArray &data) +{ + QSqlQuery *query = prepareQuery("SELECT r.id, r.replay FROM {prefix}_replays r " + "WHERE r.id_game = :game_id ORDER BY r.id DESC LIMIT 1"); + query->bindValue(":game_id", gameId); + if (!execSqlQuery(query)) { + return ReplayAccessResult::Failed; + } + if (!query->next()) { + return ReplayAccessResult::NotFound; + } + + replayId = query->value(0).toInt(); + data = query->value(1).toByteArray(); + return ReplayAccessResult::Ok; +} + +Server_DatabaseInterface::ReplayWriteResult +Servatrice_DatabaseInterface::grantReplayAccessByName(int gameId, const QString &userName) +{ + QSqlQuery *existsQuery = prepareQuery("select count(*) from {prefix}_replays_access where id_game = :idgame"); + existsQuery->bindValue(":idgame", gameId); + if (!execSqlQuery(existsQuery) || !existsQuery->next()) { + return ReplayWriteResult::Failed; + } + if (existsQuery->value(0).toInt() <= 0) { + return ReplayWriteResult::NotFound; + } + + QSqlQuery *idQuery = prepareQuery("select id from {prefix}_users WHERE name = :name"); + idQuery->bindValue(":name", userName); + if (!execSqlQuery(idQuery) || !idQuery->next()) { + return ReplayWriteResult::Failed; + } + + return addReplayAccess(gameId, idQuery->value(0).toInt(), QStringLiteral("Moderator Access Replay Grant")) + ? ReplayWriteResult::Ok + : ReplayWriteResult::Failed; +} + bool Servatrice_DatabaseInterface::getDeckFilesInFolder(int folderId, int userId, QList &files) { checkSql(); diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index 3eb24e06e..802a4022b 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -162,6 +162,12 @@ public: ReplayAccessResult getReplayData(int replayId, int userId, QByteArray &data) override; ReplayWriteResult setReplayDoNotHide(int gameId, int userId, bool doNotHide) override; ReplayWriteResult deleteReplayAccess(int gameId, int userId) override; + ReplayAccessResult checkReplayAccess(int gameId, int userId) override; + ReplayAccessResult getReplayName(int gameId, QString &replayName) override; + bool getReplayHashSource(int gameId, QByteArray &source) override; + bool addReplayAccess(int gameId, int userId, const QString &replayName) override; + ReplayAccessResult getLatestReplayForGame(int gameId, int &replayId, QByteArray &data) override; + ReplayWriteResult grantReplayAccessByName(int gameId, const QString &userName) override; /** @brief Deletes all expired share bundles (cascades to their items). */ void cleanupExpiredDeckShares(); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index 47365ea73..c73f08e5a 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -1359,19 +1359,9 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayDeleteMatch(const */ QString AbstractServerSocketInterface::createHashForReplay(int gameId) { - QSqlQuery *query = - sqlInterface->prepareQuery("select replay from {prefix}_replays where id_game = :id_game limit 3"); - query->bindValue(":id_game", gameId); - - if (!sqlInterface->execSqlQuery(query)) { - return ""; - } - QByteArray replaysBytes; - while (query->next()) { - QByteArray replay = query->value(0).toByteArray(); - replay.truncate(128); - replaysBytes.append(replay); + if (!databaseInterface->getReplayHashSource(gameId, replaysBytes)) { + return ""; } auto hash = @@ -1388,17 +1378,14 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayGetCode(const Com } // Check that user has access to replay match - { - QSqlQuery *query = sqlInterface->prepareQuery( - "select 1 from {prefix}_replays_access where id_game = :id_game and id_player = :id_player"); - query->bindValue(":id_game", cmd.game_id()); - query->bindValue(":id_player", userInfo->id()); - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; - } - if (!query->next()) { + switch (databaseInterface->checkReplayAccess(cmd.game_id(), userInfo->id())) { + case Server_DatabaseInterface::ReplayAccessResult::Ok: + break; + case Server_DatabaseInterface::ReplayAccessResult::AccessDenied: return Response::RespAccessDenied; - } + case Server_DatabaseInterface::ReplayAccessResult::NotFound: + case Server_DatabaseInterface::ReplayAccessResult::Failed: + return Response::RespInternalError; } QString hash = createHashForReplay(cmd.game_id()); @@ -1434,17 +1421,16 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const QString hash = split[1]; // Determine if the replay actually exists (and grab the replay name while at it) - auto *replayExistsQuery = - sqlInterface->prepareQuery("select replay_name from {prefix}_replays_access where id_game = :id_game limit 1"); - replayExistsQuery->bindValue(":id_game", gameId); - if (!sqlInterface->execSqlQuery(replayExistsQuery)) { - return Response::RespInternalError; + QString replayName; + switch (databaseInterface->getReplayName(gameId.toInt(), replayName)) { + case Server_DatabaseInterface::ReplayAccessResult::Ok: + break; + case Server_DatabaseInterface::ReplayAccessResult::NotFound: + case Server_DatabaseInterface::ReplayAccessResult::AccessDenied: + return Response::RespNameNotFound; + case Server_DatabaseInterface::ReplayAccessResult::Failed: + return Response::RespInternalError; } - if (!replayExistsQuery->next()) { - return Response::RespNameNotFound; - } - - const auto &replayName = replayExistsQuery->value(0).toString(); // Check if hash is correct if (hash != createHashForReplay(gameId.toInt())) { @@ -1452,26 +1438,18 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const } // Determine if user already has access to replay - auto *alreadyAccessQuery = sqlInterface->prepareQuery( - "select 1 from {prefix}_replays_access where id_game = :id_game and id_player = :id_player"); - alreadyAccessQuery->bindValue(":id_game", gameId); - alreadyAccessQuery->bindValue(":id_player", userInfo->id()); - if (!sqlInterface->execSqlQuery(alreadyAccessQuery)) { - return Response::RespInternalError; - } - if (alreadyAccessQuery->next()) { - return Response::RespOk; + switch (databaseInterface->checkReplayAccess(gameId.toInt(), userInfo->id())) { + case Server_DatabaseInterface::ReplayAccessResult::Ok: + return Response::RespOk; + case Server_DatabaseInterface::ReplayAccessResult::AccessDenied: + break; + case Server_DatabaseInterface::ReplayAccessResult::NotFound: + case Server_DatabaseInterface::ReplayAccessResult::Failed: + return Response::RespInternalError; } // Grant the User access to the replay - auto *grantReplayAccessQuery = - sqlInterface->prepareQuery("insert into {prefix}_replays_access (id_game, id_player, replay_name, do_not_hide) " - "values(:idgame, :idplayer, :replayname, 0)"); - grantReplayAccessQuery->bindValue(":idgame", gameId); - grantReplayAccessQuery->bindValue(":idplayer", userInfo->id()); - grantReplayAccessQuery->bindValue(":replayname", replayName); - - if (!sqlInterface->execSqlQuery(grantReplayAccessQuery)) { + if (!databaseInterface->addReplayAccess(gameId.toInt(), userInfo->id(), replayName)) { return Response::RespInternalError; } @@ -3043,25 +3021,22 @@ AbstractServerSocketInterface::cmdReplayDownloadByGameId(const Command_ReplayDow return Response::RespFunctionNotAllowed; } - if (!sqlInterface->checkSql()) { + if (!databaseInterface->checkSql()) { return Response::RespInternalError; } - QSqlQuery *query = sqlInterface->prepareQuery("SELECT r.id, r.replay FROM {prefix}_replays r " - "WHERE r.id_game = :game_id ORDER BY r.id DESC LIMIT 1"); - query->bindValue(":game_id", cmd.game_id()); - - if (!sqlInterface->execSqlQuery(query)) { - return Response::RespInternalError; + int replayId = 0; + QByteArray data; + switch (databaseInterface->getLatestReplayForGame(cmd.game_id(), replayId, data)) { + case Server_DatabaseInterface::ReplayAccessResult::Ok: + break; + case Server_DatabaseInterface::ReplayAccessResult::NotFound: + return Response::RespNameNotFound; + case Server_DatabaseInterface::ReplayAccessResult::AccessDenied: + case Server_DatabaseInterface::ReplayAccessResult::Failed: + return Response::RespInternalError; } - if (!query->next()) { - return Response::RespNameNotFound; - } - - int replayId = query->value(0).toInt(); - QByteArray data = query->value(1).toByteArray(); - Response_ReplayDownloadByGameId *re = new Response_ReplayDownloadByGameId; re->set_replay_data(data.data(), data.size()); re->set_replay_id(replayId); @@ -4153,47 +4128,15 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAdjustMod(const Command Response::ResponseCode AbstractServerSocketInterface::cmdGrantReplayAccess(const Command_GrantReplayAccess &cmd, ResponseContainer & /*rc*/) { - // Determine if the replay actually exists already - auto *replayExistsQuery = - sqlInterface->prepareQuery("select count(*) from {prefix}_replays_access where id_game = :idgame"); - replayExistsQuery->bindValue(":idgame", cmd.replay_id()); - if (!sqlInterface->execSqlQuery(replayExistsQuery)) { - return Response::RespInternalError; + switch (databaseInterface->grantReplayAccessByName(cmd.replay_id(), QString::fromStdString(cmd.moderator_name()))) { + case Server_DatabaseInterface::ReplayWriteResult::Ok: + return Response::RespOk; + case Server_DatabaseInterface::ReplayWriteResult::NotFound: + return Response::RespContextError; + case Server_DatabaseInterface::ReplayWriteResult::Failed: + return Response::RespInternalError; } - if (!replayExistsQuery->next()) { - return Response::RespInternalError; - } - - const auto &replayExists = replayExistsQuery->value(0).toInt() > 0; - if (!replayExists) { - return Response::RespContextError; - } - - // Determine the Moderator's User ID (As it's not apart of client, only username is) - auto *getModeratorUserIdQuery = sqlInterface->prepareQuery("select id from {prefix}_users WHERE name = :name"); - getModeratorUserIdQuery->bindValue(":name", QString::fromStdString(cmd.moderator_name())); - if (!sqlInterface->execSqlQuery(getModeratorUserIdQuery)) { - return Response::RespInternalError; - } - if (!getModeratorUserIdQuery->next()) { - return Response::RespInternalError; - } - - const auto &moderator_id = getModeratorUserIdQuery->value(0).toString(); - - // Grant the Moderator access to the replay - auto *grantReplayAccessQuery = - sqlInterface->prepareQuery("insert into {prefix}_replays_access (id_game, id_player, replay_name, do_not_hide) " - "values(:idgame, :idplayer, :replayname, 0)"); - grantReplayAccessQuery->bindValue(":idgame", cmd.replay_id()); - grantReplayAccessQuery->bindValue(":idplayer", moderator_id); - grantReplayAccessQuery->bindValue(":replayname", "Moderator Access Replay Grant"); - - if (!sqlInterface->execSqlQuery(grantReplayAccessQuery)) { - return Response::RespInternalError; - } - - return Response::RespOk; + return Response::RespInternalError; } Response::ResponseCode AbstractServerSocketInterface::cmdForceActivateUser(const Command_ForceActivateUser &cmd,