You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
Merge pull request #190 from simoleo89/fix/catalog-page-mutation-guards
fix(catalog): harden admin mutations and voucher claims
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-18
@@ -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();
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+4
-1
@@ -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);
|
||||
|
||||
+22
-13
@@ -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();
|
||||
}
|
||||
|
||||
+125
@@ -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);
|
||||
}
|
||||
}
|
||||
+39
-18
@@ -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"));
|
||||
|
||||
+8
-5
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
+43
@@ -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: "));
|
||||
}
|
||||
}
|
||||
+41
@@ -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));
|
||||
}
|
||||
}
|
||||
+57
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user