diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDeleteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDeleteEvent.java index b31194dd..64c745bd 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDeleteEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDeleteEvent.java @@ -57,8 +57,8 @@ public class FurniEditorDeleteEvent extends MessageHandler { // Check catalog_items references int catalogCount = 0; try (PreparedStatement stmt = connection.prepareStatement( - "SELECT COUNT(*) FROM catalog_items WHERE item_ids LIKE ?")) { - stmt.setString(1, "%" + id + "%"); + "SELECT COUNT(*) FROM catalog_items WHERE " + FurniEditorHelper.catalogItemIdsTokenSql("item_ids"))) { + stmt.setString(1, FurniEditorHelper.catalogItemIdsTokenPattern(id)); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { catalogCount = rs.getInt(1); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDetailEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDetailEvent.java index c4917fd1..e559633e 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDetailEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorDetailEvent.java @@ -75,8 +75,8 @@ public class FurniEditorDetailEvent extends MessageHandler { "ci.page_id AS ci_page_id, COALESCE(cp.caption, '') AS page_caption " + "FROM catalog_items ci " + "LEFT JOIN catalog_pages cp ON ci.page_id = cp.id " + - "WHERE ci.item_ids LIKE ?")) { - stmt.setString(1, "%" + itemId + "%"); + "WHERE " + FurniEditorHelper.catalogItemIdsTokenSql("ci.item_ids"))) { + stmt.setString(1, FurniEditorHelper.catalogItemIdsTokenPattern(itemId)); try (ResultSet rs = stmt.executeQuery()) { while (rs.next()) { catalogItems.add(FurniEditorHelper.readCatalogRef(rs)); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorHelper.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorHelper.java index cd7f7a82..94a8305f 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorHelper.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorHelper.java @@ -11,6 +11,15 @@ import java.util.Map; * FurniEditorSearchEvent to ensure consistent field reading. */ public class FurniEditorHelper { + public static final String CATALOG_ITEM_IDS_TOKEN_SQL = "CONCAT(',', REPLACE(item_ids, ' ', ''), ',') LIKE ?"; + + public static String catalogItemIdsTokenSql(String column) { + return "CONCAT(',', REPLACE(" + column + ", ' ', ''), ',') LIKE ?"; + } + + public static String catalogItemIdsTokenPattern(int itemId) { + return "%," + itemId + ",%"; + } /** * Read the 14 base fields from items_base into a Map. diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateEvent.java index 845395f5..e0b14f94 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdateEvent.java @@ -4,15 +4,11 @@ import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import java.sql.Connection; import java.sql.PreparedStatement; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; public class FurniEditorUpdateEvent extends MessageHandler { @@ -39,57 +35,18 @@ public class FurniEditorUpdateEvent extends MessageHandler { return; } - if (json.size() == 0) { - this.client.sendResponse(new FurniEditorResultComposer(false, "No fields to update")); + FurniEditorUpdatePayload payload = FurniEditorUpdatePayload.validate(json); + if (!payload.valid()) { + this.client.sendResponse(new FurniEditorResultComposer(false, payload.error)); return; } - // Build dynamic UPDATE with whitelisted fields - StringBuilder setClauses = new StringBuilder(); - List values = new ArrayList<>(); - - for (Map.Entry entry : json.entrySet()) { - String jsKey = entry.getKey(); - String dbColumn = FurniEditorHelper.FIELD_MAP.get(jsKey); - - if (dbColumn == null || !FurniEditorHelper.ALLOWED_UPDATE_FIELDS.contains(dbColumn)) { - continue; // Skip unknown or disallowed fields - } - - if (setClauses.length() > 0) setClauses.append(", "); - setClauses.append("`").append(dbColumn).append("` = ?"); - - JsonElement val = entry.getValue(); - if (val.isJsonPrimitive()) { - if (val.getAsJsonPrimitive().isBoolean()) { - values.add(val.getAsBoolean() ? "1" : "0"); - } else if (val.getAsJsonPrimitive().isNumber()) { - // Check if it's a decimal number - String numStr = val.getAsString(); - if (numStr.contains(".")) { - values.add(val.getAsDouble()); - } else { - values.add(val.getAsInt()); - } - } else { - values.add(val.getAsString()); - } - } else { - values.add(val.toString()); - } - } - - if (setClauses.length() == 0) { - this.client.sendResponse(new FurniEditorResultComposer(false, "No valid fields to update")); - return; - } - - String sql = "UPDATE items_base SET " + setClauses + " WHERE id = ?"; + String sql = "UPDATE items_base SET " + payload.setClauses + " WHERE id = ?"; try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement stmt = connection.prepareStatement(sql)) { int idx = 1; - for (Object value : values) { + for (Object value : payload.values) { if (value instanceof Integer) { stmt.setInt(idx++, (Integer) value); } else if (value instanceof Double) { @@ -99,7 +56,10 @@ public class FurniEditorUpdateEvent extends MessageHandler { } } stmt.setInt(idx, id); - stmt.executeUpdate(); + if (stmt.executeUpdate() == 0) { + this.client.sendResponse(new FurniEditorResultComposer(false, "Item not found: " + id)); + return; + } } // Reload emulator item definitions diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdatePayload.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdatePayload.java new file mode 100644 index 00000000..3dfb1d80 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdatePayload.java @@ -0,0 +1,133 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class FurniEditorUpdatePayload { + public final String setClauses; + public final List values; + public final String error; + + private FurniEditorUpdatePayload(String setClauses, List values, String error) { + this.setClauses = setClauses; + this.values = values; + this.error = error; + } + + public static FurniEditorUpdatePayload validate(JsonObject json) { + if (json == null || json.size() == 0) { + return invalid("No fields to update"); + } + + StringBuilder setClauses = new StringBuilder(); + List values = new ArrayList<>(); + + for (Map.Entry entry : json.entrySet()) { + String dbColumn = FurniEditorHelper.FIELD_MAP.get(entry.getKey()); + if (dbColumn == null || !FurniEditorHelper.ALLOWED_UPDATE_FIELDS.contains(dbColumn)) { + continue; + } + + Object value = validateValue(dbColumn, entry.getValue()); + if (value == null) { + return invalid("Invalid value for " + entry.getKey()); + } + + if (setClauses.length() > 0) setClauses.append(", "); + setClauses.append("`").append(dbColumn).append("` = ?"); + values.add(value); + } + + if (setClauses.length() == 0) { + return invalid("No valid fields to update"); + } + + return new FurniEditorUpdatePayload(setClauses.toString(), values, null); + } + + public boolean valid() { + return this.error == null; + } + + private static FurniEditorUpdatePayload invalid(String error) { + return new FurniEditorUpdatePayload("", List.of(), error); + } + + private static Object validateValue(String dbColumn, JsonElement element) { + if (element == null || element.isJsonNull() || !element.isJsonPrimitive()) { + return null; + } + + JsonPrimitive primitive = element.getAsJsonPrimitive(); + return switch (dbColumn) { + case "public_name" -> boundedString(primitive, 0, 56); + case "type" -> itemType(primitive); + case "width", "length" -> boundedInt(primitive, 0, 64); + case "stack_height" -> boundedDouble(primitive, 0.0D, 99.99D); + case "allow_stack", "allow_walk", "allow_sit", "allow_lay", "allow_gift", + "allow_trade", "allow_recycle", "allow_marketplace_sell", "allow_inventory_stack" -> booleanFlag(primitive); + case "interaction_type" -> boundedString(primitive, 0, 500); + case "interaction_modes_count" -> boundedInt(primitive, 0, 100); + case "vending_ids", "clothing_on_walk" -> boundedString(primitive, 0, 255); + case "customparams" -> boundedString(primitive, 0, 256); + case "multiheight" -> boundedString(primitive, 0, 50); + case "effect_id_male", "effect_id_female", "sprite_id" -> boundedInt(primitive, 0, Integer.MAX_VALUE); + case "description" -> boundedString(primitive, 0, 500); + default -> null; + }; + } + + private static String boundedString(JsonPrimitive primitive, int minLength, int maxLength) { + if (!primitive.isString()) return null; + String value = primitive.getAsString(); + if (value.length() < minLength || value.length() > maxLength) return null; + return value; + } + + private static String itemType(JsonPrimitive primitive) { + String value = boundedString(primitive, 1, 3); + if (value == null) return null; + return value.matches("[a-z]+") ? value : null; + } + + private static Integer boundedInt(JsonPrimitive primitive, int min, int max) { + try { + int value = primitive.getAsInt(); + return value >= min && value <= max ? value : null; + } catch (Exception e) { + return null; + } + } + + private static Double boundedDouble(JsonPrimitive primitive, double min, double max) { + try { + double value = primitive.getAsDouble(); + return Double.isFinite(value) && value >= min && value <= max ? value : null; + } catch (Exception e) { + return null; + } + } + + private static String booleanFlag(JsonPrimitive primitive) { + if (primitive.isBoolean()) { + return primitive.getAsBoolean() ? "1" : "0"; + } + + if (primitive.isNumber()) { + int value = primitive.getAsInt(); + return value == 0 || value == 1 ? String.valueOf(value) : null; + } + + if (primitive.isString()) { + String value = primitive.getAsString(); + return "0".equals(value) || "1".equals(value) ? value : null; + } + + return null; + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdatePayloadTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdatePayloadTest.java new file mode 100644 index 00000000..de0ba5f5 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorUpdatePayloadTest.java @@ -0,0 +1,52 @@ +package com.eu.habbo.messages.incoming.furnieditor; + +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FurniEditorUpdatePayloadTest { + @Test + void acceptsSafeEditorFields() { + FurniEditorUpdatePayload payload = FurniEditorUpdatePayload.validate(JsonParser.parseString(""" + { + "publicName": "Rare Chair", + "type": "s", + "width": 2, + "length": 1, + "stackHeight": 1.5, + "allowTrade": true, + "interactionModesCount": 3 + } + """).getAsJsonObject()); + + assertTrue(payload.valid()); + assertEquals(7, payload.values.size()); + } + + @Test + void rejectsOutOfRangeAndOversizedFields() { + assertFalse(FurniEditorUpdatePayload.validate(JsonParser.parseString("{\"width\":-1}").getAsJsonObject()).valid()); + assertFalse(FurniEditorUpdatePayload.validate(JsonParser.parseString("{\"stackHeight\":1000}").getAsJsonObject()).valid()); + assertFalse(FurniEditorUpdatePayload.validate(JsonParser.parseString("{\"allowTrade\":2}").getAsJsonObject()).valid()); + assertFalse(FurniEditorUpdatePayload.validate(JsonParser.parseString("{\"publicName\":\"" + "x".repeat(57) + "\"}").getAsJsonObject()).valid()); + } + + @Test + void ignoresUnknownFieldsButRequiresAtLeastOneValidField() { + FurniEditorUpdatePayload payload = FurniEditorUpdatePayload.validate( + JsonParser.parseString("{\"itemName\":\"blocked\",\"unknown\":true}").getAsJsonObject()); + + assertFalse(payload.valid()); + assertEquals("No valid fields to update", payload.error); + } + + @Test + void buildsCatalogItemIdsTokenPattern() { + assertEquals("%,12,%", FurniEditorHelper.catalogItemIdsTokenPattern(12)); + assertTrue((",112,12,13,").contains(",12,")); + assertFalse((",112,13,").contains(",12,")); + } +}