fix(vip): preserve safe HTML in VIP copy

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-10-19 12:52:23 +08:00
1 parent 7a0445fc4a
commit 573040381d
15 files changed
+155 -118

No files matched your search

@@ -156,9 +156,8 @@ test('header VIP hover cards match the legacy markup and style contract', () =>
assert.match(vipCardRenderer, /<div class=\"abs-center vip-baicon\"[^>]*>/)
assert.match(vipCardRenderer, /<div class=\"vip-img abs-right\">/)
assert.match(vipCardRenderer, /<div class=\"vip-name mb10\"><span class=\"mr6\">/)
assert.match(vipCardRenderer, /<ul class=\"mb10 relative\">/)
assert.match(vipCardRenderer, /aksrt_format_vip_equity_html\(\$equity, 'mb10 relative'\)/)
assert.doesNotMatch(vipCardRenderer, /vip-name__(?:icon|label)/)
assert.doesNotMatch(vipCardRenderer, /aksrt_format_vip_equity_html\(\$equity, 'mb10 relative'\)/)
assert.doesNotMatch(vipModalCss, /Header hover VIP cards/)
assert.doesNotMatch(vipModalCss, /\.navbar-vip-hover\.hover-show/)
@@ -52,7 +52,7 @@ test('弹窗文案字段更名为会员介绍/开通会员更多内容并改为
// 会员介绍为单行 input(zibll text「一句话简介」);开通会员更多内容为 rows 4 的 HTML textarea。
assert.match(
vipSchemaJs,
/key: 'pay_user_vip_desc',\s*\n\s*label: '会员介绍',\s*\n\s*type: 'input',\s*\n\s*default: '开通VIP会员,享受会员专属折扣以及多项特权'/
/key: 'pay_user_vip_desc',\s*\n\s*label: '会员介绍',\s*\n\s*type: 'input',\s*\n\s*sanitize_type: 'html',\s*\n\s*default: '开通VIP会员,享受会员专属折扣以及多项特权'/
);
assert.match(
vipSchemaJs,
@@ -60,7 +60,7 @@ test('弹窗文案字段更名为会员介绍/开通会员更多内容并改为
);
assert.match(
schemaPhp,
/'key' => 'pay_user_vip_desc', 'label' => '会员介绍', 'type' => 'input', 'default' => '开通VIP会员,享受会员专属折扣以及多项特权'/
/'key' => 'pay_user_vip_desc', 'label' => '会员介绍', 'type' => 'input', 'sanitize_type' => 'html', 'default' => '开通VIP会员,享受会员专属折扣以及多项特权'/
);
assert.match(
schemaPhp,
@@ -141,7 +141,7 @@ test('文案默认值三处一致并对齐 zibll', () => {
assert.ok(schemaPhp.includes(`'default' => '${DESC_DEFAULT}'`));
assert.ok(schemaPhp.includes(`'default' => '${MORE_DEFAULT}'`));
// PaymentRuntime 运行时回退(get_all 不合并 schema 默认值,回退即实际默认值)。
assert.ok(paymentRuntime.includes(`$desc = sanitize_text_field((string) ($settings['pay_user_vip_desc'] ?? __('${DESC_DEFAULT}', 'aksrt')))`));
assert.ok(paymentRuntime.includes(`$desc = wp_kses_post((string) ($settings['pay_user_vip_desc'] ?? __('${DESC_DEFAULT}', 'aksrt')))`));
assert.ok(paymentRuntime.includes(`$more = (string) ($settings['pay_user_vip_more'] ?? '${MORE_DEFAULT}')`));
assert.ok(paymentRuntime.includes("$name1 = __('黄金会员', 'aksrt');"));
assert.ok(paymentRuntime.includes("$name2 = __('钻石会员', 'aksrt');"));
@@ -154,19 +154,41 @@ test('文案默认值三处一致并对齐 zibll', () => {
}
});
test('权益与卡密说明字段保存时保留 HTML', () => {
// 权益字段已并入「会员购买选项」的等级 Tab,标签统一为「会员权益 HTML」。
assert.ok(vipSchemaJs.includes("key: `pay_user_vip_${level}_equity`, label: '会员权益 HTML', type: 'textarea', sanitize_type: 'html'"));
assert.ok(schemaPhp.includes("['key' => 'pay_user_vip_' . $level . '_equity', 'label' => '会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html'"));
assert.ok(rechargeJs.includes("key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', sanitize_type: 'html'"));
assert.ok(schemaPhp.includes("'key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'sanitize_type' => 'html'"));
test('会员等级名称、简介、续费和升级介绍都声明 HTML 净化', () => {
const levelFieldsJs = slice(vipSchemaJs, 'function vipLevelTabFields', 'export const vipSchema');
const levelFieldsPhp = slice(schemaPhp, 'private static function vip_level_tab_fields', 'private static function vip_product_defaults');
assert.match(levelFieldsJs, /pay_user_vip_\$\{level\}_name[^\n]*sanitize_type: 'html'/);
assert.match(levelFieldsJs, /pay_user_vip_\$\{level\}_desc[^\n]*sanitize_type: 'html'/);
assert.match(levelFieldsPhp, /pay_user_vip_' \. \$level \. '_name'[^\n]*'sanitize_type' => 'html'/);
assert.match(levelFieldsPhp, /pay_user_vip_' \. \$level \. '_desc'[^\n]*'sanitize_type' => 'html'/);
assert.match(vipSchemaJs, /key: 'vip_renew_desc',[\s\S]*?sanitize_type: 'html'/);
assert.match(schemaPhp, /'key' => 'vip_renew_desc'[^\n]*'sanitize_type' => 'html'/);
assert.match(vipSchemaJs, /key: 'vip_upgrade_desc',[^\n]*sanitize_type: 'html'/);
assert.match(schemaPhp, /'key' => 'vip_upgrade_desc'[^\n]*'sanitize_type' => 'html'/);
assert.match(vipSchemaJs, /key: 'pay_user_vip_desc',[\s\S]*?sanitize_type: 'html'/);
assert.match(schemaPhp, /'key' => 'pay_user_vip_desc'[^\n]*'sanitize_type' => 'html'/);
});
test('会员介绍运行时按单行文本净化', () => {
// 只收敛 VIP 设置函数:充值等其它设置的 $desc 与本文无关。
test('权益和促销标签 HTML 通过安全净化及 HTML 渲染保留', () => {
const vipSettings = slice(paymentRuntime, 'public static function get_payment_vip_settings', 'public static function get_payment_revenue_settings');
assert.ok(vipSettings.includes("$desc = sanitize_text_field((string) ($settings['pay_user_vip_desc']"));
assert.ok(!vipSettings.includes('$desc = sanitize_textarea_field'));
assert.match(vipSettings, /'equity'\s*=> wp_kses_post\(/);
assert.doesNotMatch(vipSettings, /\$allowed_equity/);
const equityHelper = read('inc', 'helpers', 'aksrt-vip.php');
assert.match(equityHelper, /\$clean = trim\(wp_kses_post\(\$raw\)\)/);
assert.match(payVipJs, /if \(item\.discount_tag\) append\(product, htmlNode\(/);
assert.match(payVipJs, /append\(info, htmlNode\("div", "", levelInfo\.equity\)\)/);
assert.match(payVipJs, /htmlNode\("div", "v-name", levelInfo\.name \|\| group\.name/);
assert.match(payVipJs, /htmlNode\("div", "v-desc em09", levelInfo\.desc/);
assert.match(vipTabPhp, /wp_kses_post\(\$card_desc\)/);
});
test('会员介绍按 post 级 HTML 安全净化并在弹窗与用户中心渲染', () => {
const vipSettings = slice(paymentRuntime, 'public static function get_payment_vip_settings', 'public static function get_payment_revenue_settings');
assert.match(vipSettings, /\$desc = wp_kses_post\(\(string\) \(\$settings\['pay_user_vip_desc'\]/);
assert.match(vipSettings, /'desc'\s+=> wp_kses_post\(\(string\) \(\$settings\['pay_user_vip_1_desc'\]/);
assert.match(vipSettings, /'desc'\s+=> wp_kses_post\(\(string\) \(\$settings\['pay_user_vip_2_desc'\]/);
assert.match(vipTabPhp, /wp_kses_post\(\$vip_description\)/);
assert.match(payVipJs, /htmlNode\("div", "muted-2-color em09", catalog\.desc\)/);
});
test('开通会员更多内容只在弹窗内渲染,不裸露在用户中心页面', () => {
@@ -307,9 +307,21 @@ test('积分兑换明细.level 枚举净化后仍能通过校验,非法值依
assert.strictEqual(sanitize.invalid_unit.valid, false);
assert.deepStrictEqual(sanitize.invalid_unit.errors, ['积分兑换明细[0].unit has an invalid value']);
// VIP's HTML-capable fields pass the schema save sanitizer intact.
assert.strictEqual(sanitize.vip_html.valid, true);
assert.strictEqual(sanitize.vip_html.fields.pay_user_vip_1_name, '<b>会员</b>');
assert.strictEqual(sanitize.vip_html.fields.pay_user_vip_1_desc, '<em>简介</em>');
assert.strictEqual(sanitize.vip_html.fields.pay_user_vip_1_equity, '<p>权益</p>');
assert.strictEqual(sanitize.vip_html.fields.pay_user_vip_desc, '<span>会员介绍</span>');
assert.strictEqual(sanitize.vip_html.fields.vip_renew_desc, '<strong>续费</strong>');
assert.strictEqual(sanitize.vip_html.fields.vip_upgrade_desc, '<i>升级</i>');
assert.strictEqual(sanitize.vip_html.fields.vip_1_product[0].tag, '<b>推荐</b>');
// 字符串型枚举(palette)不受影响。
assert.strictEqual(sanitize.invalid_tag_class.valid, false);
assert.deepStrictEqual(sanitize.invalid_tag_class.errors, ['会员商品[0].tag_class has an invalid value']);
});
test('运行时归一化:默认值、价格净化、零价与非法行剔除', () => {
@@ -345,7 +357,12 @@ test('运行时归一化:默认值、价格净化、零价与非法行剔除',
// 自定义续费价格类型与折扣。
assert.strictEqual(custom.renew.price_type, 'customize');
assert.strictEqual(custom.renew.discount, 7.5);
assert.strictEqual(custom.renew.desc, '续费有礼');
assert.strictEqual(custom.renew.desc, '<b>续费有礼</b>');
assert.strictEqual(custom.upgrade.desc, '<i>升级王者</i>');
assert.strictEqual(custom.levels['1'].name, '青铜会员');
assert.strictEqual(custom.levels['1'].name_html, '<strong>青铜会员</strong>');
assert.strictEqual(custom.levels['1'].desc, '<em>专属权益</em>');
assert.strictEqual(custom.levels['1'].equity, '<p>一对一支持</p>');
assert.strictEqual(custom.levels['1'].renew_products[0].tag_class, 'b-red');
// 升级三块参数透传。
assert.strictEqual(custom.upgrade.unit_price, '12.50');
@@ -23,6 +23,7 @@ namespace {
function sanitize_text_field($value): string { return trim(strip_tags((string) $value)); }
function sanitize_textarea_field($value): string { return trim((string) $value); }
function wp_kses_post($value): string { return (string) $value; }
function wp_strip_all_tags($value): string { return trim(strip_tags((string) $value)); }
function wp_kses($value, $allowed = null): string { return (string) $value; }
function trailingslashit($value): string { return rtrim((string) $value, '/\\') . '/'; }
function get_template_directory_uri(): string { return 'https://example.test/wp-content/themes/aksrt'; }
@@ -115,15 +116,17 @@ namespace {
Settings::$snapshot = [
'pay_user_vip_1_s' => true,
'pay_user_vip_2_s' => true,
'pay_user_vip_1_name' => '青铜会员',
'pay_user_vip_2_name' => '王者会员',
'pay_user_vip_1_name' => '<strong>青铜会员</strong>',
'pay_user_vip_2_name' => '<span>王者会员</span>',
'pay_user_vip_1_desc' => '<em>专属权益</em>',
'pay_user_vip_1_equity' => '<p>一对一支持</p>',
'vip_renew' => true,
'vip_renew_desc' => '续费有礼',
'vip_renew_desc' => '<b>续费有礼</b>',
'vip_renew_price_type' => 'customize',
'vip_renew_discount' => 7.5,
'vip_renew_reduce' => '15.00',
'vip_upgrade' => true,
'vip_upgrade_desc' => '升级王者',
'vip_upgrade_desc' => '<i>升级王者</i>',
'vip_upgrade_product' => [
'month_to_month_s' => true,
'unit_price' => '12.50',
@@ -179,6 +182,17 @@ namespace {
$sanitize = (static function (): array {
$provider = \AKSRT\Admin\SchemaProvider::get_instance();
$cases = [
'vip_html' => [
'pay_user_vip_1_name' => '<b>会员</b>',
'pay_user_vip_1_desc' => '<em>简介</em>',
'pay_user_vip_1_equity' => '<p>权益</p>',
'pay_user_vip_desc' => '<span>会员介绍</span>',
'vip_renew_desc' => '<strong>续费</strong>',
'vip_upgrade_desc' => '<i>升级</i>',
'vip_1_product' => [
['price' => '88.80', 'show_price' => '99.00', 'tag' => '<b>推荐</b>', 'tag_class' => 'jb-yellow', 'time' => 1, 'unit' => 'month'],
],
],
'default_rows' => [
'pay_vip_points_exchange_s' => true,
'pay_vip_points_exchange_product' => [
@@ -215,6 +229,7 @@ namespace {
$clean = $provider->sanitize_payload('payment', $payload, 'vip');
$result = $provider->validate('payment', $clean, 'vip');
$out[$name] = [
'fields' => $clean,
'points_rows' => $clean['pay_vip_points_exchange_product'] ?? null,
'valid' => $result['valid'],
'errors' => $result['errors'],