diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java index 0349184f..29e6ff11 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java @@ -711,18 +711,22 @@ public class CatalogManager { return; } - if (voucher.isExhausted()) { - client.sendResponse(new RedeemVoucherErrorComposer(Emulator.getGameEnvironment().getCatalogManager().deleteVoucher(voucher) ? RedeemVoucherErrorComposer.INVALID_CODE : RedeemVoucherErrorComposer.TECHNICAL_ERROR)); - return; + Voucher.ClaimResult claimResult = voucher.claimForUser(habbo.getHabboInfo().getId()); + switch (claimResult) { + case CLAIMED: + break; + case EXHAUSTED: + client.sendResponse(new RedeemVoucherErrorComposer(Emulator.getGameEnvironment().getCatalogManager().deleteVoucher(voucher) ? RedeemVoucherErrorComposer.INVALID_CODE : RedeemVoucherErrorComposer.TECHNICAL_ERROR)); + return; + case USER_LIMIT: + client.sendResponse(new ModToolIssueHandledComposer("You have exceeded the limit for redeeming this voucher.")); + return; + case FAILED: + default: + client.sendResponse(new RedeemVoucherErrorComposer(RedeemVoucherErrorComposer.TECHNICAL_ERROR)); + return; } - if (voucher.hasUserExhausted(habbo.getHabboInfo().getId())) { - client.sendResponse(new ModToolIssueHandledComposer("You have exceeded the limit for redeeming this voucher.")); - return; - } - - voucher.addHistoryEntry(habbo.getHabboInfo().getId()); - if (voucher.points > 0) { client.getHabbo().givePoints(voucher.pointsType, voucher.points); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/Voucher.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/Voucher.java index 468e2a2b..59cf0b5f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/Voucher.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/Voucher.java @@ -14,6 +14,13 @@ import java.util.List; public class Voucher { private static final Logger LOGGER = LoggerFactory.getLogger(Voucher.class); + public enum ClaimResult { + CLAIMED, + EXHAUSTED, + USER_LIMIT, + FAILED + } + public final int id; public final String code; public final int credits; @@ -58,18 +65,34 @@ public class Voucher { return this.amount > 0 && this.history.size() >= this.amount; } - public void addHistoryEntry(int userId) { - int timestamp = Emulator.getIntUnixTimestamp(); - this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp)); + public synchronized ClaimResult claimForUser(int userId) { + if (this.isExhausted()) { + return ClaimResult.EXHAUSTED; + } + if (this.hasUserExhausted(userId)) { + return ClaimResult.USER_LIMIT; + } + + int timestamp = Emulator.getIntUnixTimestamp(); + if (!this.insertHistoryEntry(userId, timestamp)) { + return ClaimResult.FAILED; + } + + this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp)); + return ClaimResult.CLAIMED; + } + + private boolean insertHistoryEntry(int userId, int timestamp) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO voucher_history (`voucher_id`, `user_id`, `timestamp`) VALUES (?, ?, ?)")) { statement.setInt(1, this.id); statement.setInt(2, userId); statement.setInt(3, timestamp); - statement.execute(); + return statement.executeUpdate() > 0; } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); + return false; } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java index 380a3e07..59da4f9b 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java @@ -35,33 +35,45 @@ public class CatalogAdminCreateOfferEvent extends MessageHandler { int orderNumber = this.packet.readInt(); CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + CatalogAdminOfferPayload payload = CatalogAdminOfferPayload.validate(pageId, itemIds, catalogName, costCredits, + costPoints, pointsType, amount, clubOnly, extradata, haveOffer, offerIdGroup, limitedStack, + orderNumber, pageType); + if (payload == null) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Invalid offer payload")); + return; + } + + if (Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(payload.pageId, payload.pageType) == null) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + payload.pageId)); + return; + } + int newId = -1; try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - (pageType == CatalogPageType.BUILDER) + (payload.pageType == CatalogPageType.BUILDER) ? "INSERT INTO catalog_items_bc (page_id, item_ids, catalog_name, order_number, extradata) VALUES (?, ?, ?, ?, ?)" : "INSERT INTO catalog_items (page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, club_only, extradata, have_offer, offer_id, limited_stack, order_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { - String cleanItemIds = (itemIds == null || itemIds.trim().isEmpty()) ? "0" : itemIds.trim(); - statement.setInt(1, pageId); - statement.setString(2, cleanItemIds); - statement.setString(3, catalogName); + statement.setInt(1, payload.pageId); + statement.setString(2, payload.itemIds); + statement.setString(3, payload.catalogName); - if (pageType == CatalogPageType.BUILDER) { - statement.setInt(4, orderNumber); - statement.setString(5, extradata); + if (payload.pageType == CatalogPageType.BUILDER) { + statement.setInt(4, payload.orderNumber); + statement.setString(5, payload.extradata); } else { - statement.setInt(4, costCredits); - statement.setInt(5, costPoints); - statement.setInt(6, pointsType); - statement.setInt(7, amount); - statement.setString(8, clubOnly == 1 ? "1" : "0"); - statement.setString(9, extradata); - statement.setString(10, haveOffer ? "1" : "0"); - statement.setInt(11, offerIdGroup); - statement.setInt(12, limitedStack); - statement.setInt(13, orderNumber); + statement.setInt(4, payload.costCredits); + statement.setInt(5, payload.costPoints); + statement.setInt(6, payload.pointsType); + statement.setInt(7, payload.amount); + statement.setString(8, payload.clubOnly == 1 ? "1" : "0"); + statement.setString(9, payload.extradata); + statement.setString(10, payload.haveOffer ? "1" : "0"); + statement.setInt(11, payload.offerIdGroup); + statement.setInt(12, payload.limitedStack); + statement.setInt(13, payload.orderNumber); } statement.execute(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java index 420e21c0..879eb9b0 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java @@ -36,7 +36,7 @@ public class CatalogAdminCreatePageEvent extends MessageHandler { pageLayout = CatalogPageLayouts.default_3x3; } - if (parentId != -1 && Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId) == null) { + if (parentId != -1 && Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId, pageType) == null) { this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + parentId)); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java index c72f0273..f44c08cf 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java @@ -36,7 +36,10 @@ public class CatalogAdminDeletePageEvent extends MessageHandler { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement(query)) { statement.setInt(1, pageId); - statement.execute(); + if (statement.executeUpdate() == 0) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); + return; + } } Emulator.getGameEnvironment().getCatalogManager().getCatalogPagesMap(pageType).remove(pageId); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java index c88725c0..0e2a524f 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java @@ -28,12 +28,21 @@ public class CatalogAdminMovePageEvent extends MessageHandler { CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); String tableName = (pageType == CatalogPageType.BUILDER) ? "catalog_pages_bc" : "catalog_pages"; + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType); + if (page == null) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); + return; + } + if (newParentId == -1) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( "UPDATE " + tableName + " SET enabled = IF(enabled = '1', '0', '1') WHERE id = ?")) { statement.setInt(1, pageId); - statement.execute(); + if (statement.executeUpdate() == 0) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); + return; + } } this.client.sendResponse(new CatalogAdminResultComposer(true, "Page toggled")); return; @@ -44,30 +53,27 @@ public class CatalogAdminMovePageEvent extends MessageHandler { PreparedStatement statement = connection.prepareStatement( "UPDATE " + tableName + " SET visible = IF(visible = '1', '0', '1') WHERE id = ?")) { statement.setInt(1, pageId); - statement.execute(); + if (statement.executeUpdate() == 0) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); + return; + } } this.client.sendResponse(new CatalogAdminResultComposer(true, "Visibility toggled")); return; } - - CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType); - if (page == null) { - this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); - return; - } if (newParentId == pageId) { this.client.sendResponse(new CatalogAdminResultComposer(false, "A page cannot be its own parent")); return; } - CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(newParentId); + CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(newParentId, pageType); if (parent == null) { this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + newParentId)); return; } - if (this.wouldCreateCycle(pageId, newParentId)) { + if (this.wouldCreateCycle(pageId, newParentId, pageType)) { this.client.sendResponse(new CatalogAdminResultComposer(false, "Refusing to move: that would create a cycle")); return; } @@ -80,18 +86,21 @@ public class CatalogAdminMovePageEvent extends MessageHandler { statement.setInt(1, newParentId); statement.setInt(2, newIndex); statement.setInt(3, pageId); - statement.execute(); + if (statement.executeUpdate() == 0) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); + return; + } } this.client.sendResponse(new CatalogAdminResultComposer(true, "Page moved")); } - private boolean wouldCreateCycle(int pageId, int parentId) { + private boolean wouldCreateCycle(int pageId, int parentId, CatalogPageType pageType) { int current = parentId; for (int hops = 0; hops < MAX_PARENT_WALK; hops++) { if (current == ROOT_PARENT_ID) return false; if (current == pageId) return true; - CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(current); + CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(current, pageType); if (parent == null) return false; current = parent.getParentId(); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminOfferPayload.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminOfferPayload.java new file mode 100644 index 00000000..43073036 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminOfferPayload.java @@ -0,0 +1,125 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import com.eu.habbo.habbohotel.catalog.CatalogPageType; + +final class CatalogAdminOfferPayload { + private static final int MAX_ITEM_IDS_LENGTH = 512; + private static final int MAX_ITEM_IDS = 100; + private static final int MAX_CATALOG_NAME_LENGTH = 128; + private static final int MAX_EXTRADATA_LENGTH = 1024; + private static final int MAX_CURRENCY_VALUE = 1_000_000_000; + private static final int MAX_AMOUNT = 10_000; + private static final int MAX_POINTS_TYPE = 10_000; + private static final int MAX_ORDER_NUMBER = 1_000_000; + private static final int MAX_LIMITED_STACK = 1_000_000; + + final int pageId; + final String itemIds; + final String catalogName; + final int costCredits; + final int costPoints; + final int pointsType; + final int amount; + final int clubOnly; + final String extradata; + final boolean haveOffer; + final int offerIdGroup; + final int limitedStack; + final int orderNumber; + final CatalogPageType pageType; + + private CatalogAdminOfferPayload(int pageId, String itemIds, String catalogName, int costCredits, int costPoints, + int pointsType, int amount, int clubOnly, String extradata, boolean haveOffer, + int offerIdGroup, int limitedStack, int orderNumber, CatalogPageType pageType) { + this.pageId = pageId; + this.itemIds = itemIds; + this.catalogName = catalogName; + this.costCredits = costCredits; + this.costPoints = costPoints; + this.pointsType = pointsType; + this.amount = amount; + this.clubOnly = clubOnly; + this.extradata = extradata; + this.haveOffer = haveOffer; + this.offerIdGroup = offerIdGroup; + this.limitedStack = limitedStack; + this.orderNumber = orderNumber; + this.pageType = pageType; + } + + static CatalogAdminOfferPayload validate(int pageId, String itemIds, String catalogName, int costCredits, + int costPoints, int pointsType, int amount, int clubOnly, + String extradata, boolean haveOffer, int offerIdGroup, + int limitedStack, int orderNumber, CatalogPageType pageType) { + String cleanItemIds = normalizeItemIds(itemIds); + String cleanCatalogName = clamp(catalogName, MAX_CATALOG_NAME_LENGTH); + String cleanExtradata = clamp(extradata, MAX_EXTRADATA_LENGTH); + + if (pageId <= 0 + || cleanItemIds == null + || cleanCatalogName.isBlank() + || !isInRange(orderNumber, 0, MAX_ORDER_NUMBER)) { + return null; + } + + if (pageType != CatalogPageType.BUILDER) { + if (!isInRange(costCredits, 0, MAX_CURRENCY_VALUE) + || !isInRange(costPoints, 0, MAX_CURRENCY_VALUE) + || !isInRange(pointsType, 0, MAX_POINTS_TYPE) + || !isInRange(amount, 1, MAX_AMOUNT) + || !isInRange(clubOnly, 0, 1) + || offerIdGroup < 0 + || !isInRange(limitedStack, 0, MAX_LIMITED_STACK)) { + return null; + } + } + + return new CatalogAdminOfferPayload(pageId, cleanItemIds, cleanCatalogName, costCredits, costPoints, + pointsType, amount, clubOnly, cleanExtradata, haveOffer, offerIdGroup, limitedStack, orderNumber, + pageType); + } + + private static String normalizeItemIds(String value) { + if (value == null || value.trim().isEmpty()) { + return "0"; + } + + String clean = value.trim(); + if (clean.length() > MAX_ITEM_IDS_LENGTH) { + return null; + } + + String[] parts = clean.split(","); + if (parts.length == 0 || parts.length > MAX_ITEM_IDS) { + return null; + } + + for (String part : parts) { + if (part.isBlank()) { + return null; + } + + try { + if (Integer.parseInt(part.trim()) < 0) { + return null; + } + } catch (NumberFormatException e) { + return null; + } + } + + return clean.replaceAll("\\s+", ""); + } + + private static boolean isInRange(int value, int min, int max) { + return value >= min && value <= max; + } + + private static String clamp(String value, int maxLength) { + if (value == null) { + return ""; + } + + return value.length() <= maxLength ? value : value.substring(0, maxLength); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java index 1a5aff1a..816d31c5 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java @@ -34,10 +34,28 @@ public class CatalogAdminSaveOfferEvent extends MessageHandler { int orderNumber = this.packet.readInt(); CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString()); + if (offerId <= 0) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Invalid offer id")); + return; + } + + CatalogAdminOfferPayload payload = CatalogAdminOfferPayload.validate(pageId, itemIds, catalogName, costCredits, + costPoints, pointsType, amount, clubOnly, extradata, haveOffer, offerIdGroup, limitedStack, + orderNumber, pageType); + if (payload == null) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Invalid offer payload")); + return; + } + + if (Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(payload.pageId, payload.pageType) == null) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + payload.pageId)); + return; + } + boolean updateItemIds = itemIds != null && !itemIds.trim().isEmpty(); String sql; - if (pageType == CatalogPageType.BUILDER) { + if (payload.pageType == CatalogPageType.BUILDER) { sql = updateItemIds ? "UPDATE catalog_items_bc SET page_id = ?, item_ids = ?, catalog_name = ?, order_number = ?, extradata = ? WHERE id = ?" : "UPDATE catalog_items_bc SET page_id = ?, catalog_name = ?, order_number = ?, extradata = ? WHERE id = ?"; @@ -50,30 +68,33 @@ public class CatalogAdminSaveOfferEvent extends MessageHandler { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement(sql)) { int idx = 1; - statement.setInt(idx++, pageId); + statement.setInt(idx++, payload.pageId); if (updateItemIds) { - statement.setString(idx++, itemIds.trim()); + statement.setString(idx++, payload.itemIds); } - statement.setString(idx++, catalogName); + statement.setString(idx++, payload.catalogName); - if (pageType == CatalogPageType.BUILDER) { - statement.setInt(idx++, orderNumber); - statement.setString(idx++, extradata); + if (payload.pageType == CatalogPageType.BUILDER) { + statement.setInt(idx++, payload.orderNumber); + statement.setString(idx++, payload.extradata); statement.setInt(idx, offerId); } else { - statement.setInt(idx++, costCredits); - statement.setInt(idx++, costPoints); - statement.setInt(idx++, pointsType); - statement.setInt(idx++, amount); - statement.setString(idx++, clubOnly == 1 ? "1" : "0"); - statement.setString(idx++, extradata); - statement.setString(idx++, haveOffer ? "1" : "0"); - statement.setInt(idx++, offerIdGroup); - statement.setInt(idx++, limitedStack); - statement.setInt(idx++, orderNumber); + statement.setInt(idx++, payload.costCredits); + statement.setInt(idx++, payload.costPoints); + statement.setInt(idx++, payload.pointsType); + statement.setInt(idx++, payload.amount); + statement.setString(idx++, payload.clubOnly == 1 ? "1" : "0"); + statement.setString(idx++, payload.extradata); + statement.setString(idx++, payload.haveOffer ? "1" : "0"); + statement.setInt(idx++, payload.offerIdGroup); + statement.setInt(idx++, payload.limitedStack); + statement.setInt(idx++, payload.orderNumber); statement.setInt(idx, offerId); } - statement.execute(); + if (statement.executeUpdate() == 0) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Offer not found: " + offerId)); + return; + } } this.client.sendResponse(new CatalogAdminResultComposer(true, "Offer saved")); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java index 9fc808b1..05dbbc9e 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java @@ -74,13 +74,13 @@ public class CatalogAdminSavePageEvent extends MessageHandler { return; } - CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId); + CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(parentId, pageType); if (parent == null) { this.client.sendResponse(new CatalogAdminResultComposer(false, "Parent page not found: " + parentId)); return; } - if (this.wouldCreateCycle(pageId, parentId)) { + if (this.wouldCreateCycle(pageId, parentId, pageType)) { this.client.sendResponse(new CatalogAdminResultComposer(false, "Refusing to re-parent: that would create a cycle")); return; } @@ -144,18 +144,21 @@ public class CatalogAdminSavePageEvent extends MessageHandler { statement.setInt(15, pageId); } - statement.execute(); + if (statement.executeUpdate() == 0) { + this.client.sendResponse(new CatalogAdminResultComposer(false, "Page not found: " + pageId)); + return; + } } this.client.sendResponse(new CatalogAdminResultComposer(true, "Page saved")); } - private boolean wouldCreateCycle(int pageId, int parentId) { + private boolean wouldCreateCycle(int pageId, int parentId, CatalogPageType pageType) { int current = parentId; for (int hops = 0; hops < MAX_PARENT_WALK; hops++) { if (current == ROOT_PARENT_ID) return false; if (current == pageId) return true; - CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(current); + CatalogPage parent = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(current, pageType); if (parent == null) return false; current = parent.getParentId(); } diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/catalog/VoucherClaimContractTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/catalog/VoucherClaimContractTest.java new file mode 100644 index 00000000..c3639ec6 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/catalog/VoucherClaimContractTest.java @@ -0,0 +1,50 @@ +package com.eu.habbo.habbohotel.catalog; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +class VoucherClaimContractTest { + private static String voucherSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/catalog/Voucher.java")); + } + + private static String catalogManagerSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java")); + } + + @Test + void voucherClaimIsSynchronizedAndPersistsBeforeRewardEligibility() throws Exception { + String source = voucherSource(); + + assertTrue(source.contains("public synchronized ClaimResult claimForUser(int userId)"), + "voucher claim should check limits and persist history under a per-voucher lock"); + assertTrue(source.contains("private boolean insertHistoryEntry"), + "history insert should report database failure to the caller"); + + int insertCall = source.indexOf("insertHistoryEntry(userId, timestamp)"); + int memoryAppend = source.indexOf("this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp))"); + + assertTrue(insertCall > -1, "claimForUser must persist the history row"); + assertTrue(memoryAppend > insertCall, + "in-memory history must only be updated after the database insert succeeds"); + } + + @Test + void catalogRewardsOnlyAfterVoucherClaimSucceeds() throws Exception { + String source = catalogManagerSource(); + + int claim = source.indexOf("Voucher.ClaimResult claimResult = voucher.claimForUser"); + int claimedGuard = source.indexOf("case CLAIMED", claim); + int pointsGrant = source.indexOf("client.getHabbo().givePoints", claim); + int creditsGrant = source.indexOf("client.getHabbo().giveCredits", claim); + + assertTrue(claim > -1, "CatalogManager must claim the voucher before applying rewards"); + assertTrue(claimedGuard > claim, "voucher rewards should only continue for a CLAIMED result"); + assertTrue(pointsGrant > claimedGuard, "points must be granted only after CLAIMED"); + assertTrue(creditsGrant > claimedGuard, "credits must be granted only after CLAIMED"); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminOfferMutationContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminOfferMutationContractTest.java new file mode 100644 index 00000000..901922ca --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminOfferMutationContractTest.java @@ -0,0 +1,43 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +class CatalogAdminOfferMutationContractTest { + private static final Path CREATE_SOURCE = Path.of( + "src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreateOfferEvent.java"); + private static final Path SAVE_SOURCE = Path.of( + "src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSaveOfferEvent.java"); + + @Test + void createAndSaveValidatePayloadAndTargetPageBeforeWriting() throws IOException { + String create = Files.readString(CREATE_SOURCE); + String save = Files.readString(SAVE_SOURCE); + + assertTrue(create.contains("CatalogAdminOfferPayload.validate(")); + assertTrue(save.contains("CatalogAdminOfferPayload.validate(")); + assertTrue(create.contains("getCatalogPage(payload.pageId, payload.pageType) == null")); + assertTrue(save.contains("getCatalogPage(payload.pageId, payload.pageType) == null")); + + int createValidation = create.indexOf("CatalogAdminOfferPayload.validate("); + int createInsert = create.indexOf("INSERT INTO catalog_items"); + int saveValidation = save.indexOf("CatalogAdminOfferPayload.validate("); + int saveUpdate = save.indexOf("UPDATE catalog_items"); + + assertTrue(createValidation < createInsert, "create offer should validate before insert SQL is prepared"); + assertTrue(saveValidation < saveUpdate, "save offer should validate before update SQL is prepared"); + } + + @Test + void saveOfferReportsMissingRowsInsteadOfAlwaysSucceeding() throws IOException { + String save = Files.readString(SAVE_SOURCE); + + assertTrue(save.contains("statement.executeUpdate() == 0")); + assertTrue(save.contains("Offer not found: ")); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminOfferPayloadTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminOfferPayloadTest.java new file mode 100644 index 00000000..8b2d71b4 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminOfferPayloadTest.java @@ -0,0 +1,41 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.eu.habbo.habbohotel.catalog.CatalogPageType; +import org.junit.jupiter.api.Test; + +class CatalogAdminOfferPayloadTest { + @Test + void acceptsAndNormalizesValidOfferPayload() { + CatalogAdminOfferPayload payload = CatalogAdminOfferPayload.validate( + 42, "1, 2,3", "Rare Chair", 100, 5, 0, 1, 0, + "extra", true, 0, 0, 10, CatalogPageType.NORMAL); + + assertNotNull(payload); + assertEquals("1,2,3", payload.itemIds); + assertEquals("Rare Chair", payload.catalogName); + } + + @Test + void rejectsInvalidItemIdsAndNegativeEconomyValues() { + assertNull(CatalogAdminOfferPayload.validate(42, "1,abc", "Name", 0, 0, 0, 1, 0, + "", false, 0, 0, 0, CatalogPageType.NORMAL)); + assertNull(CatalogAdminOfferPayload.validate(42, "1", "Name", -1, 0, 0, 1, 0, + "", false, 0, 0, 0, CatalogPageType.NORMAL)); + assertNull(CatalogAdminOfferPayload.validate(42, "1", "Name", 0, 0, 0, 0, 0, + "", false, 0, 0, 0, CatalogPageType.NORMAL)); + } + + @Test + void builderOffersStillRequireSafeCommonFields() { + assertNotNull(CatalogAdminOfferPayload.validate(42, "", "BC Offer", -1, -1, -1, -1, -1, + "", false, -1, -1, 0, CatalogPageType.BUILDER)); + assertNull(CatalogAdminOfferPayload.validate(0, "1", "BC Offer", 0, 0, 0, 1, 0, + "", false, 0, 0, 0, CatalogPageType.BUILDER)); + assertNull(CatalogAdminOfferPayload.validate(42, "1", "", 0, 0, 0, 1, 0, + "", false, 0, 0, 0, CatalogPageType.BUILDER)); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminPageMutationContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminPageMutationContractTest.java new file mode 100644 index 00000000..f97b0d66 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminPageMutationContractTest.java @@ -0,0 +1,57 @@ +package com.eu.habbo.messages.incoming.catalog.catalogadmin; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CatalogAdminPageMutationContractTest { + private static final Path CREATE_SOURCE = Path.of( + "src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminCreatePageEvent.java"); + private static final Path SAVE_SOURCE = Path.of( + "src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminSavePageEvent.java"); + private static final Path MOVE_SOURCE = Path.of( + "src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminMovePageEvent.java"); + private static final Path DELETE_SOURCE = Path.of( + "src/main/java/com/eu/habbo/messages/incoming/catalog/catalogadmin/CatalogAdminDeletePageEvent.java"); + + @Test + void pageParentChecksStayWithinTheSameCatalogPageType() throws IOException { + String create = Files.readString(CREATE_SOURCE); + String save = Files.readString(SAVE_SOURCE); + String move = Files.readString(MOVE_SOURCE); + + assertTrue(create.contains("getCatalogPage(parentId, pageType)")); + assertTrue(save.contains("getCatalogPage(parentId, pageType)")); + assertTrue(save.contains("getCatalogPage(current, pageType)")); + assertTrue(move.contains("getCatalogPage(newParentId, pageType)")); + assertTrue(move.contains("getCatalogPage(current, pageType)")); + } + + @Test + void movePageValidatesTargetBeforeTogglingVisibilityOrEnabledState() throws IOException { + String move = Files.readString(MOVE_SOURCE); + + int pageLookup = move.indexOf("getCatalogPage(pageId, pageType)"); + int enabledToggle = move.indexOf("SET enabled = IF"); + int visibleToggle = move.indexOf("SET visible = IF"); + + assertTrue(pageLookup >= 0, "move page should load the page before mutating it"); + assertTrue(pageLookup < enabledToggle, "enabled toggle must not run before page existence is checked"); + assertTrue(pageLookup < visibleToggle, "visible toggle must not run before page existence is checked"); + } + + @Test + void pageMutationsReportMissingRowsInsteadOfAlwaysSucceeding() throws IOException { + String save = Files.readString(SAVE_SOURCE); + String move = Files.readString(MOVE_SOURCE); + String delete = Files.readString(DELETE_SOURCE); + + assertTrue(save.contains("statement.executeUpdate() == 0")); + assertTrue(move.contains("statement.executeUpdate() == 0")); + assertTrue(delete.contains("statement.executeUpdate() == 0")); + } +}