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
fix(catalog): validate admin offer payloads
This commit is contained in:
+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();
|
||||
|
||||
|
||||
+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"));
|
||||
|
||||
+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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user