From 1152d9ba4810432e949d89dc1e0531b433a86c32 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Mon, 13 Jul 2026 01:00:00 +0300 Subject: [PATCH] feat: Update room cloning API to accept multiple content types for name input; enhance compatibility with 2023 client --- DorkNet.Server.Tests/RoomSave2023Tests.cs | 5 ++- .../API/Rooms/V2/RoomsModerationController.cs | 42 ++++++++++++++++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/DorkNet.Server.Tests/RoomSave2023Tests.cs b/DorkNet.Server.Tests/RoomSave2023Tests.cs index d847bf5..1eba7f0 100644 --- a/DorkNet.Server.Tests/RoomSave2023Tests.cs +++ b/DorkNet.Server.Tests/RoomSave2023Tests.cs @@ -186,9 +186,12 @@ public sealed class RoomSave2023Tests : IClassFixture private static async Task CloneAndAssertAsync(HttpClient client, long sourceId, string name) { + // The real 2023 client posts the name as x-www-form-urlencoded + // (`name=...`), NOT JSON — testing with JSON would miss the 415 that + // a [FromBody] handler returns for a form POST. using var resp = await client.PostAsync( $"/rooms/{sourceId}/clone", - new StringContent(JsonSerializer.Serialize(new { name }), Encoding.UTF8, "application/json")); + new FormUrlEncodedContent(new Dictionary { ["name"] = name })); var text = await resp.Content.ReadAsStringAsync(); Assert.True(resp.IsSuccessStatusCode, $"clone of {sourceId} -> {(int)resp.StatusCode}: {text}"); diff --git a/DorkNet.Server/Controllers/API/Rooms/V2/RoomsModerationController.cs b/DorkNet.Server/Controllers/API/Rooms/V2/RoomsModerationController.cs index 1fb8a5c..eceb225 100644 --- a/DorkNet.Server/Controllers/API/Rooms/V2/RoomsModerationController.cs +++ b/DorkNet.Server/Controllers/API/Rooms/V2/RoomsModerationController.cs @@ -959,12 +959,17 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase /// ; /// this handler proxies via the service so we don't duplicate the /// substantial clone-rooms logic. + // The 2023 client posts the new name as an x-www-form-urlencoded field + // (`name=...`), NOT JSON. A `[FromBody]` parameter demands a JSON + // content-type and makes ASP.NET reject the form POST with 415 BEFORE the + // handler runs → the client reports "Failed to clone room" and boots the + // player to their dorm. Accept all three content types and read the name + // manually from form / JSON body / query. [HttpPost("rooms/{roomId:long}/clone")] - public async Task BareClone(long roomId, - [FromBody] CloneBareRequest? body, - [FromForm(Name = "Name")] string? formName) + [Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")] + public async Task BareClone(long roomId) { - var newName = body?.Name ?? formName ?? string.Empty; + var newName = await ReadCloneNameAsync(); var source = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId); if (source is null) return NotFound(); if (!source.CloningAllowed && source.CreatorPlayerId != Me) @@ -1084,7 +1089,34 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase // room". return Ok(RoomsController.BuildRoomServerDetails(clone, clonedScenes)); } - public sealed class CloneBareRequest { public string? Name { get; set; } } + private async Task ReadCloneNameAsync() + { + // Form: name=... (what the 2023 client sends). + if (Request.HasFormContentType) + { + foreach (var k in new[] { "name", "Name" }) + { + var v = Request.Form[k].ToString(); + if (!string.IsNullOrWhiteSpace(v)) return v; + } + } + else + { + // JSON body: { "name": "..." }. + try + { + Request.EnableBuffering(); + Request.Body.Position = 0; + using var doc = await JsonDocument.ParseAsync(Request.Body); + if (doc.RootElement.ValueKind == JsonValueKind.Object) + foreach (var k in new[] { "name", "Name" }) + if (doc.RootElement.TryGetProperty(k, out var v) && v.ValueKind == JsonValueKind.String) + return v.GetString() ?? string.Empty; + } + catch { /* non-JSON / empty body */ } + } + return Request.Query["name"].ToString(); + } // ── Bare-path bans aliases ─────────────────────────────────────────