From 4e62b325d21e8cbca711d440a764cfe60e3d6764 Mon Sep 17 00:00:00 2001 From: Xing Date: Sun, 1 Nov 2026 15:47:09 +0800 Subject: [PATCH] fix(badges): keep the medal catalog from being cast to a string on save MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit user_medal_args was declared as type=json without sanitize_type, so sanitize_field_value() fell through to sanitize_text_field((string) $value) and stored the literal string "Array". validate() has no json shape check, so the save reported success. Consequence: parse_medal_catalog() json_decoded "Array" into an empty catalog, the dictionary came back empty, and user_context() dropped every owned badge — the front end showed "获取徽章" and the grant modal listed nothing, hiding both the newly added and the pre-existing badges. Add the missing sanitize_type to match its sibling level/integral fields, and guard sanitize_field_value() so structured container types can never reach the scalar fallback again. Mirrors the declaration in the Vue schema. Co-Authored-By: Claude Opus 5 (1M context) --- assets/admin/src/schema/sections/users/badge.js | 1 + src/Admin/Schema/SchemaUsers.php | 4 ++++ src/Admin/SchemaProvider.php | 7 +++++++ 3 files changed, 12 insertions(+) diff --git a/assets/admin/src/schema/sections/users/badge.js b/assets/admin/src/schema/sections/users/badge.js index 0e5c8b3b..2e8c476c 100644 --- a/assets/admin/src/schema/sections/users/badge.js +++ b/assets/admin/src/schema/sections/users/badge.js @@ -134,6 +134,7 @@ export const badgeSchema = { key: 'user_medal_args', label: '分类与条目', type: 'json', + sanitizeType: 'array', default: defaultMedalArgs(), help: '由自定义页编辑:分类 → 条目(slug/名称/描述/图标/get_type/get_val)。仅手动授予用 manually_add。', dependsOn: { key: 'user_medal_s', value: true }, diff --git a/src/Admin/Schema/SchemaUsers.php b/src/Admin/Schema/SchemaUsers.php index 2a8e9eaa..4fecd3ec 100644 --- a/src/Admin/Schema/SchemaUsers.php +++ b/src/Admin/Schema/SchemaUsers.php @@ -557,6 +557,10 @@ class SchemaUsers { 'key' => 'user_medal_args', 'label' => '分类与条目', 'type' => 'json', + // Must be explicit: sanitize_field_value() only honours sanitize_type + // on the write path. Without it the nested catalog is cast to the + // literal string "Array", wiping every badge from the dictionary. + 'sanitize_type' => 'array', 'default' => self::default_medal_args(), 'help' => '结构化目录:分类 → 条目(slug/name/desc/icon/get_type/get_val)。由 SPA UsersBadge 编辑。', ], diff --git a/src/Admin/SchemaProvider.php b/src/Admin/SchemaProvider.php index 7f209c4f..6da9b49a 100644 --- a/src/Admin/SchemaProvider.php +++ b/src/Admin/SchemaProvider.php @@ -857,6 +857,13 @@ class SchemaProvider { return esc_url_raw((string) $value); } + // Structured container types must never reach the scalar fallback below: + // casting a nested payload to string silently destroys it ("Array") and + // validate() has no json/array shape check to catch the loss. + if (in_array($type, ['json', 'repeater', 'sortable', 'group', 'fieldset', 'accordion'], true)) { + return is_array($value) ? $value : []; + } + return sanitize_text_field((string) $value); }