You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-20 15:36:17 +00:00
Merge pull request #188 from simoleo89/fix/furnieditor-update-validation
fix(furni-editor): validate and sync furnidata changes
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package com.eu.habbo.habbohotel.items;
|
||||
|
||||
/**
|
||||
* Builds a complete furnidata entry object (single-line JSON5) from an {@link Item}
|
||||
* (its items_base row) plus a display name/description. Used by the Furni Editor
|
||||
* upsert path when a furni has no furnidata entry yet. Field shape mirrors the
|
||||
* hotel's existing furnidata entries; {@code id} is the item's sprite id so the
|
||||
* renderer resolves the furni's name/data by typeId.
|
||||
*/
|
||||
public final class FurnidataEntryBuilder {
|
||||
|
||||
private FurnidataEntryBuilder() {}
|
||||
|
||||
public static String build(Item item, String name, String description) {
|
||||
String classname = item.getName() != null ? item.getName() : "";
|
||||
String safeName = (name != null && !name.isBlank()) ? name
|
||||
: (item.getFullName() != null && !item.getFullName().isBlank()) ? item.getFullName()
|
||||
: classname;
|
||||
String safeDesc = description != null ? description : "";
|
||||
String customParams = item.getCustomParams() != null ? item.getCustomParams() : "";
|
||||
|
||||
StringBuilder b = new StringBuilder(256);
|
||||
b.append("{\"id\":").append(item.getSpriteId());
|
||||
b.append(",\"classname\":\"").append(esc(classname)).append('"');
|
||||
b.append(",\"revision\":0,\"category\":\"unknown\",\"defaultdir\":0");
|
||||
b.append(",\"xdim\":").append(item.getWidth());
|
||||
b.append(",\"ydim\":").append(item.getLength());
|
||||
b.append(",\"partcolors\":{\"color\":[]}");
|
||||
b.append(",\"name\":\"").append(esc(safeName)).append('"');
|
||||
b.append(",\"description\":\"").append(esc(safeDesc)).append('"');
|
||||
b.append(",\"adurl\":\"\",\"offerid\":-1,\"buyout\":false,\"rentofferid\":-1,\"rentbuyout\":false,\"bc\":false,\"excludeddynamic\":false");
|
||||
b.append(",\"customparams\":\"").append(esc(customParams)).append('"');
|
||||
b.append(",\"specialtype\":1");
|
||||
b.append(",\"canstandon\":").append(item.allowWalk());
|
||||
b.append(",\"cansiton\":").append(item.allowSit());
|
||||
b.append(",\"canlayon\":").append(item.allowLay());
|
||||
b.append('}');
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
/** Escape for a JSON string value; collapse control chars to spaces. */
|
||||
private static String esc(String v) {
|
||||
StringBuilder b = new StringBuilder(v.length() + 8);
|
||||
for (int i = 0; i < v.length(); i++) {
|
||||
char c = v.charAt(i);
|
||||
if (c == '"' || c == '\\') b.append('\\').append(c);
|
||||
else if (c == '\n' || c == '\r' || c == '\t') b.append(' ');
|
||||
else b.append(c);
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
}
|
||||
@@ -36,30 +36,36 @@ public final class FurnidataSourceResolver {
|
||||
public static Source resolve() {
|
||||
try {
|
||||
String override = Emulator.getConfig().getValue("items.furnidata.path", "");
|
||||
if (!override.isEmpty()) {
|
||||
Path p = Paths.get(override);
|
||||
if (Files.exists(p)) return new Source(p, Files.isDirectory(p), Status.RESOLVED, "items.furnidata.path");
|
||||
return new Source(p, Files.isDirectory(p), Status.SOURCE_MISSING, "items.furnidata.path does not exist");
|
||||
}
|
||||
|
||||
String rendererConfigPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", "");
|
||||
String assetBasePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||
|
||||
if (!rendererConfigPath.isEmpty()) {
|
||||
Source fromRenderer = resolveFromRendererConfig(Paths.get(rendererConfigPath), assetBasePath.isEmpty() ? null : Paths.get(assetBasePath));
|
||||
if (fromRenderer.ok() || fromRenderer.status() == Status.UNRESOLVED_PLACEHOLDER) return fromRenderer;
|
||||
}
|
||||
|
||||
Source fallback = resolveFromAssetBase(assetBasePath);
|
||||
if (fallback != null) return fallback;
|
||||
|
||||
return new Source(null, false, Status.CONFIG_MISSING, "No furnidata source config found");
|
||||
return resolveConfigured(override, rendererConfigPath, assetBasePath);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("FurnidataSourceResolver failed", e);
|
||||
return new Source(null, false, Status.ERROR, e.getMessage() != null ? e.getMessage() : "Resolver error");
|
||||
}
|
||||
}
|
||||
|
||||
public static Source resolveConfigured(String legacyOverridePath, String rendererConfigPath, String assetBasePath) {
|
||||
if (rendererConfigPath != null && !rendererConfigPath.isEmpty()) {
|
||||
Source fromRenderer = resolveFromRendererConfig(Paths.get(rendererConfigPath), assetBasePath == null || assetBasePath.isEmpty() ? null : Paths.get(assetBasePath));
|
||||
if (fromRenderer.ok() || fromRenderer.status() == Status.UNRESOLVED_PLACEHOLDER) return fromRenderer;
|
||||
}
|
||||
|
||||
Source fromAssetBase = resolveFromAssetBase(assetBasePath);
|
||||
if (fromAssetBase != null && fromAssetBase.ok()) return fromAssetBase;
|
||||
|
||||
if (legacyOverridePath != null && !legacyOverridePath.isEmpty()) {
|
||||
Path p = Paths.get(legacyOverridePath);
|
||||
if (Files.exists(p)) return new Source(p, Files.isDirectory(p), Status.RESOLVED, "items.furnidata.path fallback");
|
||||
return new Source(p, Files.isDirectory(p), Status.SOURCE_MISSING, "items.furnidata.path fallback does not exist");
|
||||
}
|
||||
|
||||
if (fromAssetBase != null) return fromAssetBase;
|
||||
|
||||
return new Source(null, false, Status.CONFIG_MISSING, "No furnidata source config found");
|
||||
}
|
||||
|
||||
public static Source resolveFromRendererConfig(Path rendererConfig, Path assetBase) {
|
||||
try {
|
||||
if (rendererConfig == null || !Files.exists(rendererConfig)) {
|
||||
|
||||
@@ -56,6 +56,98 @@ public class FurnidataWriter {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Outcome of a {@link #create} attempt. */
|
||||
public enum CreateResult { CREATED, ALREADY_EXISTS, ID_COLLISION, NO_TARGET, IO_ERROR }
|
||||
|
||||
/**
|
||||
* Append a brand-new furnidata entry (upsert's "create" half). Refuses if the
|
||||
* classname already exists (caller should edit instead) or if {@code id} is
|
||||
* already used by a DIFFERENT classname (id collision would break the
|
||||
* {@code roomItem.name.<id>} / typeId resolution on the renderer). The complete
|
||||
* entry object is built by the caller (see FurnidataEntryBuilder) and inserted
|
||||
* right after the opening '[' of the matching section's "furnitype" array.
|
||||
*
|
||||
* @param classname new classname (must be absent from furnidata)
|
||||
* @param id furnidata id (= item sprite id); must not collide
|
||||
* @param type FLOOR -> roomitemtypes, WALL -> wallitemtypes
|
||||
* @param entryJson5 the complete entry object as a single-line JSON5 string
|
||||
* @param createTier split-tier only: the tier dir to write into (e.g. "custom"); ignored for single-file
|
||||
*/
|
||||
public CreateResult create(String classname, int id, FurnitureType type, String entryJson5, String createTier) {
|
||||
String cn = classname == null ? "" : classname.trim().toLowerCase(java.util.Locale.ROOT);
|
||||
if (cn.isEmpty() || entryJson5 == null || entryJson5.isBlank()) return CreateResult.NO_TARGET;
|
||||
|
||||
// Guard: duplicate classname / id collision (scan the whole source).
|
||||
for (FurnidataEntry e : new FurnidataReader(source, maxBytes).read()) {
|
||||
String ecn = e.classname() == null ? "" : e.classname().trim().toLowerCase(java.util.Locale.ROOT);
|
||||
if (ecn.equals(cn)) return CreateResult.ALREADY_EXISTS;
|
||||
if (e.id() == id) return CreateResult.ID_COLLISION;
|
||||
}
|
||||
|
||||
try {
|
||||
Path target = resolveCreateTarget(createTier);
|
||||
if (target == null) return CreateResult.NO_TARGET;
|
||||
|
||||
String raw = Files.readString(target, StandardCharsets.UTF_8);
|
||||
String section = (type == FurnitureType.WALL) ? "wallitemtypes" : "roomitemtypes";
|
||||
int open = furnitypeArrayOpenIndex(raw, section);
|
||||
if (open < 0) return CreateResult.NO_TARGET; // section/array absent in target file
|
||||
|
||||
String edited = raw.substring(0, open) + "\n" + entryJson5 + "," + raw.substring(open);
|
||||
backup(target);
|
||||
atomicWrite(target, edited);
|
||||
return CreateResult.CREATED;
|
||||
} catch (IOException e) {
|
||||
return CreateResult.IO_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
/** Single-file: the source. Split-tier: the create-tier file (created with a shell if absent). */
|
||||
private Path resolveCreateTarget(String createTier) throws IOException {
|
||||
if (!directory) return source;
|
||||
String tier = (createTier == null || createTier.isBlank()) ? "custom" : createTier.trim();
|
||||
Path base = source.toAbsolutePath().normalize();
|
||||
Path tierDir = safeResolve(base, tier);
|
||||
if (tierDir == null) return null;
|
||||
if (!Files.isDirectory(tierDir)) Files.createDirectories(tierDir);
|
||||
for (String fileName : manifestList(tierDir, "files", List.of())) {
|
||||
Path f = safeResolve(base, tierDir.resolve(fileName).toString());
|
||||
if (f != null && Files.isRegularFile(f)) return f;
|
||||
}
|
||||
Path def = tierDir.resolve("furnidata.json5");
|
||||
if (!Files.exists(def)) {
|
||||
Files.writeString(def,
|
||||
"{\n \"roomitemtypes\": { \"furnitype\": [\n] },\n \"wallitemtypes\": { \"furnitype\": [\n] }\n}\n",
|
||||
StandardCharsets.UTF_8);
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
/** Index just after the '[' that opens {@code <section>.furnitype}, or -1 if absent. String-aware. */
|
||||
static int furnitypeArrayOpenIndex(String raw, String section) {
|
||||
int s = indexOfKey(raw, section, 0);
|
||||
if (s < 0) return -1;
|
||||
int ft = indexOfKey(raw, "furnitype", s);
|
||||
if (ft < 0) return -1;
|
||||
boolean inStr = false; char q = 0;
|
||||
for (int i = ft; i < raw.length(); i++) {
|
||||
char c = raw.charAt(i);
|
||||
if (inStr) { if (c == '\\') i++; else if (c == q) inStr = false; continue; }
|
||||
if (c == '"' || c == '\'') { inStr = true; q = c; }
|
||||
else if (c == '[') return i + 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** First occurrence of a quoted key ("key" or 'key') at/after {@code from}, or -1. */
|
||||
private static int indexOfKey(String raw, String key, int from) {
|
||||
int a = raw.indexOf("\"" + key + "\"", from);
|
||||
int b = raw.indexOf("'" + key + "'", from);
|
||||
if (a < 0) return b;
|
||||
if (b < 0) return a;
|
||||
return Math.min(a, b);
|
||||
}
|
||||
|
||||
/** For single-file just returns the file; for split-tier, the tier file that contains cn. */
|
||||
private Path locateFile(String cn) throws IOException {
|
||||
if (!directory) {
|
||||
|
||||
@@ -27,6 +27,7 @@ public class FurnitureTextProvider {
|
||||
private final boolean enabled;
|
||||
private volatile Map<String, FurniText> index = Map.of();
|
||||
private volatile Path source;
|
||||
private volatile String sourceDescription = "unknown";
|
||||
private FurnidataWatcher watcher;
|
||||
|
||||
public FurnitureTextProvider(boolean enabled) {
|
||||
@@ -47,7 +48,7 @@ public class FurnitureTextProvider {
|
||||
return;
|
||||
}
|
||||
reindex(new FurnidataReader(this.source, DEFAULT_MAX_BYTES).read());
|
||||
LOGGER.info("FurnitureTextProvider: indexed {} furnidata names from {}", this.index.size(), this.source);
|
||||
LOGGER.info("Furniture Text Provider -> Indexed! ({} names, source: {})", this.index.size(), this.sourceDescription);
|
||||
|
||||
if (Boolean.parseBoolean(Emulator.getConfig().getValue("items.furnidata.watch.enabled", "true"))) {
|
||||
if (this.watcher != null) this.watcher.stop();
|
||||
@@ -88,9 +89,12 @@ public class FurnitureTextProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private static Path resolveSource() {
|
||||
private Path resolveSource() {
|
||||
FurnidataSourceResolver.Source source = FurnidataSourceResolver.resolve();
|
||||
if (source.ok()) return source.path();
|
||||
if (source.ok()) {
|
||||
this.sourceDescription = source.message();
|
||||
return source.path();
|
||||
}
|
||||
LOGGER.warn("FurnitureTextProvider: no furnidata source resolved ({}) - {}", source.status(), source.message());
|
||||
return null;
|
||||
}
|
||||
|
||||
+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
|
||||
|
||||
+35
-3
@@ -5,6 +5,8 @@ import com.eu.habbo.habbohotel.items.FurnidataEntry;
|
||||
import com.eu.habbo.habbohotel.items.FurnidataLock;
|
||||
import com.eu.habbo.habbohotel.items.FurnidataWriter;
|
||||
import com.eu.habbo.habbohotel.items.FurnitureTextProvider;
|
||||
import com.eu.habbo.habbohotel.items.Item;
|
||||
import com.eu.habbo.habbohotel.items.FurnidataEntryBuilder;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
@@ -109,6 +111,7 @@ public class FurniEditorUpdateFurnidataEvent extends MessageHandler {
|
||||
String safeDesc = (description != null) ? description : "";
|
||||
|
||||
boolean written;
|
||||
boolean created = false;
|
||||
List<FurnidataEntry> delta;
|
||||
|
||||
FurnidataLock.LOCK.lock();
|
||||
@@ -121,8 +124,37 @@ public class FurniEditorUpdateFurnidataEvent extends MessageHandler {
|
||||
);
|
||||
written = writer.write(classname, safeName, safeDesc);
|
||||
if (!written) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Classname not found in furnidata"));
|
||||
return;
|
||||
// Upsert: no furnidata entry for this classname yet → create a
|
||||
// complete one seeded from items_base (id = sprite id).
|
||||
Item item = Emulator.getGameEnvironment().getItemManager().getItem(itemId);
|
||||
if (item == null) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Item not found"));
|
||||
return;
|
||||
}
|
||||
String createTier = Emulator.getConfig().getValue("items.furnidata.create_tier", "custom");
|
||||
String entry = FurnidataEntryBuilder.build(
|
||||
item,
|
||||
FurnitureTextProvider.sanitize(safeName),
|
||||
FurnitureTextProvider.sanitize(safeDesc));
|
||||
FurnidataWriter.CreateResult cr =
|
||||
writer.create(item.getName(), item.getSpriteId(), item.getType(), entry, createTier);
|
||||
switch (cr) {
|
||||
case CREATED:
|
||||
created = true;
|
||||
written = true;
|
||||
break;
|
||||
case ALREADY_EXISTS:
|
||||
// entry already present (race / no-op edit) — apply the edit and treat as success
|
||||
writer.write(classname, safeName, safeDesc);
|
||||
written = true;
|
||||
break;
|
||||
case ID_COLLISION:
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Sprite id already used by another classname"));
|
||||
return;
|
||||
default:
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Failed to create furnidata entry"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
delta = provider.reindexFromSource();
|
||||
@@ -161,7 +193,7 @@ public class FurniEditorUpdateFurnidataEvent extends MessageHandler {
|
||||
FurnidataAuditLog.record(
|
||||
adminId,
|
||||
classname,
|
||||
"edit",
|
||||
created ? "create" : "edit",
|
||||
oldName != null ? oldName : "",
|
||||
FurnitureTextProvider.sanitize(safeName),
|
||||
oldDesc,
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user