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(furnieditor): validate item update payloads
This commit is contained in:
+2
-2
@@ -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);
|
||||
|
||||
+2
-2
@@ -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));
|
||||
|
||||
+9
@@ -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.
|
||||
|
||||
+9
-49
@@ -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<Object> values = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, JsonElement> 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
|
||||
|
||||
+133
@@ -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<Object> values;
|
||||
public final String error;
|
||||
|
||||
private FurniEditorUpdatePayload(String setClauses, List<Object> 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<Object> values = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, JsonElement> 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;
|
||||
}
|
||||
}
|
||||
+52
@@ -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,"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user