feat(dashboard): check uploaded character XML before storing it

The XML upload is the one place hand-written XML reaches the game, whose
load path trusts the XML because the server writes it itself. Instead of
making the game skip bad data at load, the upload is checked against
what the load path assumes and refused (400, with the problems) when the
game couldn't load it: required elements, attributes that must parse
(flags read with std::stoul/stoull), required item and mission fields,
known inventory types, mission states and character versions, items and
missions that exist in the CDClient, unique item IDs and slots, and the
acct attribute matching the owner.

Suspicious but loadable content is returned as warnings that need
confirm=true (409 otherwise): contraband (same matching as the world,
now shared in ContrabandRules.h), stacks above the stack size, coins,
level or u-score out of reach, a GM level above the account's.
Contraband marked flag-and-remove is removed only if the uploader asks;
once stored, findings are flagged (CONTRABAND) and audited. The XML
editor shows the findings and offers "Save anyway". Related: issue 1332.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 08:21:53 -05:00
parent 0b88d5b5c5
commit 6c414cec48
9 changed files with 743 additions and 59 deletions

View File

@@ -85,20 +85,6 @@ namespace {
}
namespace Contraband {
std::vector<Finding> Find(const std::vector<HeldItem>& items, const List& list) {
std::vector<Finding> found;
if (list.empty()) return found;
for (const auto& item : items) {
const auto it = list.find(item.lot);
if (it != list.end() && item.count > 0) found.push_back({ item, it->second });
}
return found;
}
bool Applies(eGameMasterLevel accountLevel, bool ignoreStaff) {
return !ignoreStaff || accountLevel <= eGameMasterLevel::CIVILIAN;
}
bool CountsAsAdded(eLootSourceType source, eInventoryType sourceInventory) {
return sourceInventory == eInventoryType::INVALID && source != eLootSourceType::RELOCATE && source != eLootSourceType::INVENTORY;
}

View File

@@ -9,10 +9,9 @@
#include "dCommonVars.h"
#include "eInventoryType.h"
#include "eLootSourceType.h"
#include "IContraband.h"
#include "ContrabandRules.h"
class Entity;
enum class eGameMasterLevel : uint8_t;
/**
* Contraband (issue #1563): items staff don't want players to have, listed on the dashboard's Contraband page
@@ -29,32 +28,7 @@ enum class eGameMasterLevel : uint8_t;
* Staff accounts (GM level above civilian) are not checked unless contraband_ignore_staff is 0.
*/
namespace Contraband {
struct Entry {
std::string reason;
IContraband::eContrabandAction action{};
};
using List = std::map<LOT, Entry>;
struct HeldItem {
LWOOBJID id{};
LOT lot{};
uint32_t count{};
eInventoryType inventory{};
};
struct Finding {
HeldItem item;
Entry entry;
};
// ---- Pure rules, unit tested ----
// The held items that are on the list, in the order given
std::vector<Finding> Find(const std::vector<HeldItem>& items, const List& list);
// Whether an account at this GM level is checked
bool Applies(eGameMasterLevel accountLevel, bool ignoreStaff);
// ---- Pure rules, unit tested (Entry, List, HeldItem, Finding, Find and Applies are in ContrabandRules.h) ----
// Whether an added item should be checked: moves between a player's own inventories are not new items
bool CountsAsAdded(eLootSourceType source, eInventoryType sourceInventory);