diff --git a/assets/admin/src/schema/sections/payment/vip.js b/assets/admin/src/schema/sections/payment/vip.js index 334dbda7..5c1930ec 100644 --- a/assets/admin/src/schema/sections/payment/vip.js +++ b/assets/admin/src/schema/sections/payment/vip.js @@ -69,9 +69,9 @@ function vipProductFields() { function vipLevelTabFields(level) { const dep = { key: `pay_user_vip_${level}_s`, value: true }; return [ - { key: `pay_user_vip_${level}_name`, label: '会员显示名称', type: 'input', default: level === 1 ? '黄金会员' : '钻石会员', dependsOn: dep, help: '会员名称(例如“黄金会员”、“超级会员”)' }, - { key: `pay_user_vip_${level}_desc`, label: '一句话简介', type: 'input', default: '尊享专属特权', dependsOn: dep }, - { key: `pay_user_vip_${level}_equity`, label: '会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: VIP_EQUITY_DEFAULT, rows: 4, dependsOn: dep, help: '建议使用
  • 列表项' }, + { key: `pay_user_vip_${level}_name`, label: '会员显示名称', type: 'input', sanitize_type: 'html', default: level === 1 ? '黄金会员' : '钻石会员', dependsOn: dep, help: '支持HTML,请注意控制长度' }, + { key: `pay_user_vip_${level}_desc`, label: '一句话简介', type: 'input', sanitize_type: 'html', default: '尊享专属特权', dependsOn: dep, help: '支持HTML' }, + { key: `pay_user_vip_${level}_equity`, label: '会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: VIP_EQUITY_DEFAULT, rows: 4, dependsOn: dep, help: '支持安全HTML,建议使用
  • 列表项' }, { key: `vip_${level}_product_s`, label: '购买会员', type: 'switch', default: true, dependsOn: dep, help: '关闭后则不能付费购买会员,但可通过积分兑换会员,请确保已开启积分以及积分兑换会员功能' }, { key: `vip_${level}_product`, @@ -290,9 +290,10 @@ export const vipSchema = { key: 'vip_renew_desc', label: '续费介绍', type: 'input', + sanitize_type: 'html', default: '立即续费会员,畅享VIP权益', dependsOn: { key: 'vip_renew', value: true }, - help: '续费会员的一句话简介', + help: '续费会员的一句话简介,支持HTML', }, { key: 'vip_renew_price_type', @@ -371,7 +372,7 @@ export const vipSchema = { title: '会员升级', fields: [ { key: 'vip_upgrade', label: '允许升级', type: 'switch', default: true }, - { key: 'vip_upgrade_desc', label: '升级介绍', type: 'input', default: '升级VIP会员,享更多会员权益', dependsOn: { key: 'vip_upgrade', value: true }, help: '升级会员的一句话简介' }, + { key: 'vip_upgrade_desc', label: '升级介绍', type: 'input', sanitize_type: 'html', default: '升级VIP会员,享更多会员权益', dependsOn: { key: 'vip_upgrade', value: true }, help: '升级会员的一句话简介,支持HTML' }, { key: 'vip_upgrade_product', label: '升级价格', @@ -478,9 +479,10 @@ export const vipSchema = { key: 'pay_user_vip_desc', label: '会员介绍', type: 'input', + sanitize_type: 'html', default: '开通VIP会员,享受会员专属折扣以及多项特权', dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] }, - help: '显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍', + help: '显示在开通界面顶部一句话简介,可以为会员权益简介或者活动介绍,支持HTML', }, { key: 'pay_user_vip_more', diff --git a/assets/js/pay-vip.js b/assets/js/pay-vip.js index 8d3a8b58..78c29f11 100644 --- a/assets/js/pay-vip.js +++ b/assets/js/pay-vip.js @@ -17,8 +17,7 @@ return el; } - // 服务端已按 post 级 HTML 净化的富文本字段(more / 卡密说明)走 innerHTML; - // node() 的 textContent 会把标签当字面文本显示。 + // Server-side schema sanitization limits these configured rich-text fields to post-safe HTML. function htmlNode(tag, className, value) { var el = document.createElement(tag); if (className) el.className = className; @@ -70,7 +69,7 @@ if (levelInfo && levelInfo.img_icon) { var img = node("img", "img-icon"); img.src = text(levelInfo.img_icon); - img.alt = text(levelInfo.name || ""); + img.alt = text(levelInfo.name_text || levelInfo.name || ""); img.decoding = "async"; return img; } @@ -125,7 +124,7 @@ // (右上 tag + 价格行 + 时长 badge,无标题行)。 var product = node("div", "zib-widget vip-product relative text-center product-box"); // 标签颜色取后台「标签颜色」配置,未配置时回退参考主题默认的 jb-yellow。 - if (item.discount_tag) append(product, node("div", "abs-right vip-tag badg " + (item.tag_class || "jb-yellow"), item.discount_tag)); + if (item.discount_tag) append(product, htmlNode("div", "abs-right vip-tag badg " + (item.tag_class || "jb-yellow"), item.discount_tag)); append(product, priceRow(mark, item.price_label || "", item.show_price_label || "")); if (item.days_label) append(product, node("div", "muted-2-color c-blue em09 p2-10 badg radius", item.days_label)); append(label, product); @@ -408,7 +407,7 @@ } else if (member.state === "expired") { append(userInfo, node("div", "muted-2-color em09", "已过期")); } else if (catalog.desc) { - append(userInfo, node("div", "muted-2-color em09", catalog.desc)); + append(userInfo, htmlNode("div", "muted-2-color em09", catalog.desc)); } append(userBox, userInfo); append(modal, userBox); @@ -444,8 +443,8 @@ append(miniIcon, vipIcon(levelInfo)); append(miniIconbox, miniIcon); var miniInfo = node("div", "v-info"); - append(miniInfo, node("div", "v-name", actionLabel(action) + (levelInfo.name || group.name || "VIP" + level))); - append(miniInfo, node("div", "v-desc em09", levelInfo.desc || "")); + append(miniInfo, htmlNode("div", "v-name", actionLabel(action) + (levelInfo.name || group.name || "VIP" + level))); + append(miniInfo, htmlNode("div", "v-desc em09", levelInfo.desc || "")); append(miniIconbox, miniInfo); append(miniCard, miniIconbox); append(tab, miniCard); @@ -471,15 +470,12 @@ append(vicon, vipIcon(levelInfo)); append(iconbox, vicon); var vinfo = node("div", "v-info"); - append(vinfo, node("div", "v-name", levelInfo.name || group.name || "VIP" + level)); - append(vinfo, node("div", "v-desc em09", levelInfo.desc || "")); + append(vinfo, htmlNode("div", "v-name", levelInfo.name || group.name || "VIP" + level)); + append(vinfo, htmlNode("div", "v-desc em09", levelInfo.desc || "")); append(iconbox, vinfo); append(info, iconbox); if (levelInfo.equity) { - var equity = document.createElement("ul"); - equity.className = "v-equity"; - equity.innerHTML = text(levelInfo.equity); - append(info, equity); + append(info, htmlNode("div", "", levelInfo.equity)); } var infoBa = node("div", "abs-center vip-baicon"); append(infoBa, vipIcon(levelInfo)); diff --git a/inc/helpers/aksrt-header-userbox.php b/inc/helpers/aksrt-header-userbox.php index 1213e503..e75fbb23 100644 --- a/inc/helpers/aksrt-header-userbox.php +++ b/inc/helpers/aksrt-header-userbox.php @@ -200,7 +200,7 @@ if (!function_exists('aksrt_header_payvip_bar')) : $level = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($user_id) : 0; if ($level > 0) { $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; - $name = (string) ($levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt'))); + $name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ($level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt'))); // 到期徽章语义对齐参考主题 zib_get_user_vip_exp_date_text():只有真的拿到 // 到期时间才渲染这枚徽章,已过期显示「会员已过期」否则显示裸日期;显式永久 // 会员显示「永久会员」;拿不到到期数据时参考主题返回 false,整枚徽章不渲染。 @@ -219,7 +219,7 @@ if (!function_exists('aksrt_header_payvip_bar')) : $html = '
    '; $html .= ''; $html .= function_exists('aksrt_get_vip_icon') ? aksrt_get_vip_icon($user_id, 'em12 mr6', false) : ''; - $html .= '' . esc_html($name) . ''; + $html .= '' . wp_kses_post($name) . ''; if ($exp_label !== '') { $html .= '' . '' diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php index 7b83f092..672f3890 100644 --- a/inc/helpers/aksrt-pay.php +++ b/inc/helpers/aksrt-pay.php @@ -1309,6 +1309,7 @@ if (!function_exists('aksrt_get_posts_vip_price')) { } $level_name = (string) ($level_settings['name'] ?? sprintf(__('VIP%d', 'aksrt'), $level)); + $level_name_html = (string) ($level_settings['name_html'] ?? $level_name); $price = $vip_minor === 0 ? esc_html__('免费', 'aksrt') : $mark . esc_html(aksrt_format_price($value)); @@ -1319,7 +1320,7 @@ if (!function_exists('aksrt_get_posts_vip_price')) { ? ' data-toggle="tooltip" title="' . esc_attr($title) . '"' : ''; - $items[] = '' . $icon . esc_html($level_name) . '' . $price . ''; + $items[] = '' . $icon . wp_kses_post($level_name_html) . '' . $price . ''; } $html = implode('', $items); @@ -1384,10 +1385,13 @@ if (!function_exists('aksrt_get_paywall_html')) { $vip_action = $vip_level > 0 ? sprintf(__('升级%s', 'aksrt'), $level_name) : __('开通会员', 'aksrt'); + $vip_action_html = $vip_level > 0 + ? __('升级', 'aksrt') . wp_kses_post((string) ($level_settings['name_html'] ?? $level_name)) + : esc_html__('开通会员', 'aksrt'); $action_class = is_user_logged_in() ? 'pay-vip' : 'signin-loader'; $icon = aksrt_get_pay_vip_icon_html($level_settings); $button = '
    ' . esc_html(sprintf(__('您暂无购买权限,请先%s', 'aksrt'), $vip_action)) . '
    '; - $button .= '' . $icon . esc_html($vip_action) . ''; + $button .= '' . $icon . $vip_action_html . ''; } elseif (!is_user_logged_in()) { if ($amount_minor > 0 && $modo === '0' && aksrt_allow_guest_content_pay($post)) { $button = ''; diff --git a/inc/helpers/aksrt-vip.php b/inc/helpers/aksrt-vip.php index 1d664ab5..62355d11 100644 --- a/inc/helpers/aksrt-vip.php +++ b/inc/helpers/aksrt-vip.php @@ -481,7 +481,7 @@ endif; * or Chinese keyword heuristics — that mangles intentional HTML. * * Rules: - * - Sanitize with a small allowlist. + * - Sanitize with WordPress post-safe HTML. * - If markup already has
  • , wrap the items in