From b11b096828627c58fcb2ffcbcda33d0a48e383e4 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:50:43 +0200 Subject: [PATCH] [Server] Apply card art rules to playmats (#7532) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Server] Apply card art rules to playmats Card art DENY rules were only enforced for user card art (SetCardArtParams) while playmats using the same art bypassed them. Move the rule lookup to Servatrice_DatabaseInterface::isCardArtAllowed with a default-allow base on Server_DatabaseInterface, delegate the session check to it, reject DENY-listed Command_SetPlaymat with RespFunctionNotAllowed, and strip DENY-listed deck playmats on DeckSelect so banned art is never broadcast. * [Server] Address review nits on playmat card art rules --------- Co-authored-by: Lukas BrĂ¼bach --- .../server/remote/game/server_player.cpp | 21 ++++++++++++++++++ .../server/remote/server_database_interface.h | 10 +++++++++ .../src/servatrice_database_interface.cpp | 22 +++++++++++++++++++ .../src/servatrice_database_interface.h | 1 + servatrice/src/serversocketinterface.cpp | 22 +------------------ servatrice/src/serversocketinterface.h | 1 - 6 files changed, 55 insertions(+), 22 deletions(-) diff --git a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp index a4c904c01..949d04b4b 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp +++ b/libcockatrice_network/libcockatrice/network/server/remote/game/server_player.cpp @@ -257,6 +257,19 @@ Server_Player::cmdDeckSelect(const Command_DeckSelect &cmd, ResponseContainer &r return Response::RespInternalError; } + // Card art rules apply to playmats as well: never broadcast a DENY-listed + // printing as a playmat. Stripping keeps the deck selectable while hiding + // the banned art from all clients. + const auto deckPlaymat = newDeck->getPlaymat(); + if (!deckPlaymat.card.name.isEmpty()) { + auto *db = game->getRoom()->getServer()->getDatabaseInterface(); + // Check the truncated values actually broadcast below; the rule columns are varchar(255). + if (db && !db->isCardArtAllowed(deckPlaymat.card.name.left(MAX_NAME_LENGTH), + deckPlaymat.card.providerId.left(MAX_NAME_LENGTH))) { + newDeck->setPlaymat({}); + } + } + delete deck; deck = newDeck; sideboardLocked = true; @@ -631,6 +644,14 @@ Server_Player::cmdSetPlaymat(const Command_SetPlaymat &cmd, ResponseContainer &r if (rawName.length() > MAX_NAME_LENGTH || rawProviderId.length() > MAX_NAME_LENGTH) { return Response::RespInvalidData; } + // Card art rules apply to playmats as well: reject DENY-listed art, like + // cmdSetCardArtParams does. Empty clears the playmat and is always allowed. + if (!rawName.isEmpty()) { + auto *db = game->getRoom()->getServer()->getDatabaseInterface(); + if (db && !db->isCardArtAllowed(rawName, rawProviderId)) { + return Response::RespFunctionNotAllowed; + } + } PlaymatInfo playmat; playmat.card.name = rawName; playmat.card.providerId = rawProviderId; 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 e0be7990b..167adfeaf 100644 --- a/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h +++ b/libcockatrice_network/libcockatrice/network/server/remote/server_database_interface.h @@ -188,6 +188,16 @@ public: virtual void setForcePasswordChange(const QString & /* user */, bool /* force */) { } + /** + * @brief Whether the given card art may be shown (card art rules). + * + * Default allows everything; the real server overrides this with a lookup + * in {prefix}_card_art_name_rules. Empty names are always allowed (clear path). + */ + virtual bool isCardArtAllowed(const QString & /* cardName */, const QString & /* providerId */) + { + return true; + } }; #endif diff --git a/servatrice/src/servatrice_database_interface.cpp b/servatrice/src/servatrice_database_interface.cpp index ac3a19c8e..e7d46c050 100644 --- a/servatrice/src/servatrice_database_interface.cpp +++ b/servatrice/src/servatrice_database_interface.cpp @@ -2068,3 +2068,25 @@ void Servatrice_DatabaseInterface::addAuditRecord(const QString &user, query->bindValue(":details", details); execSqlQuery(query); } + +bool Servatrice_DatabaseInterface::isCardArtAllowed(const QString &cardName, const QString &cardProviderId) +{ + if (cardName.isEmpty() || !checkSql()) { + return true; + } + QSqlQuery *q = prepareQuery( + "SELECT mode FROM {prefix}_card_art_name_rules WHERE card_name = :name AND card_provider_id = :provider"); + q->bindValue(":name", cardName); + q->bindValue(":provider", cardProviderId); + + if (!execSqlQuery(q)) { + qWarning() << "Card art rule lookup failed; failing open for" << cardName; + return true; + } + + if (!q->next()) { + return true; // default allow + } + + return q->value(0).toString() != "DENY"; +} diff --git a/servatrice/src/servatrice_database_interface.h b/servatrice/src/servatrice_database_interface.h index 0509d1a03..411360fa7 100644 --- a/servatrice/src/servatrice_database_interface.h +++ b/servatrice/src/servatrice_database_interface.h @@ -218,6 +218,7 @@ public: }; UptimeSnapshot getLatestUptimeSnapshot(int serverId); bool removeUserAvatar(const QString &userName); + bool isCardArtAllowed(const QString &cardName, const QString &cardProviderId) override; bool addForgotPassword(const QString &user); bool removeForgotPassword(const QString &user) override; bool doesForgotPasswordExist(const QString &user); diff --git a/servatrice/src/serversocketinterface.cpp b/servatrice/src/serversocketinterface.cpp index bf8d02fc7..b4d12599d 100644 --- a/servatrice/src/serversocketinterface.cpp +++ b/servatrice/src/serversocketinterface.cpp @@ -3481,26 +3481,6 @@ Response::ResponseCode AbstractServerSocketInterface::cmdAccountImage(const Comm return Response::RespOk; } -bool AbstractServerSocketInterface::isCardNameAllowed(const QString &cardName, const QString &cardProviderId) -{ - QSqlQuery *q = sqlInterface->prepareQuery( - "SELECT mode FROM {prefix}_card_art_name_rules WHERE card_name = :name AND card_provider_id = :provider"); - - q->bindValue(":name", cardName); - q->bindValue(":provider", cardProviderId); - - if (!sqlInterface->execSqlQuery(q)) { - qWarning() << "Card art rule lookup failed; failing open for" << cardName; - return true; - } - - if (!q->next()) { - return true; // default allow - } - - return q->value(0).toString() != "DENY"; -} - Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const Command_SetCardArtParams &cmd, ResponseContainer & /* rc */) { @@ -3527,7 +3507,7 @@ Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const return Response::RespOk; } - if (!isCardNameAllowed(cardName, cardProviderId)) { + if (!sqlInterface->isCardArtAllowed(cardName, cardProviderId)) { return Response::RespFunctionNotAllowed; } diff --git a/servatrice/src/serversocketinterface.h b/servatrice/src/serversocketinterface.h index 7de742c66..586bff831 100644 --- a/servatrice/src/serversocketinterface.h +++ b/servatrice/src/serversocketinterface.h @@ -216,7 +216,6 @@ private: Response::ResponseCode cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer &rc); Response::ResponseCode cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer &rc); - bool isCardNameAllowed(const QString &cardName, const QString &cardProviderId); Response::ResponseCode cmdSetCardArtParams(const Command_SetCardArtParams &cmd, ResponseContainer &); Response::ResponseCode cmdAddCardArtRule(const Command_AddCardArtRule &cmd, ResponseContainer &); Response::ResponseCode cmdRemoveCardArtRule(const Command_RemoveCardArtRule &cmd, ResponseContainer &);