fix(vip): render 开通会员更多内容 as HTML in the modal
Three layers were dropping the markup: the save path was already relaxed to wp_kses_post via schema sanitize_type html, but the runtime output re-filtered more through the narrow equity whitelist (h/img/hr/table stripped), and pay-vip.js inserted it with node() whose textContent showed the tags as literal text. The card-pass desc had the same output-side strip despite its save-side fix. - PaymentRuntime: more and pay_vip_pass_charge_desc now output via wp_kses_post, matching the save-time level (repo precedent: revenue note, after-sale desc) - pay-vip.js: new htmlNode() helper (innerHTML) for server-sanitized rich text; more and cardPass.desc use it, plain-text fields keep node() Verification: 3 new contract tests cover output-level kses and innerHTML rendering; node --test tests/**/*.test.cjs 640/640 pass; php -l and node --check clean.
This commit is contained in:
1 parent
817ac9c647
commit
2e2558e5ce
3 files changed
+33
-4
No files matched your search
+11
-2
@@ -17,6 +17,15 @@
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 服务端已按 post 级 HTML 净化的富文本字段(more / 卡密说明)走 innerHTML;
|
||||||
|
// node() 的 textContent 会把标签当字面文本显示。
|
||||||
|
function htmlNode(tag, className, value) {
|
||||||
|
var el = document.createElement(tag);
|
||||||
|
if (className) el.className = className;
|
||||||
|
if (value !== undefined && value !== null) el.innerHTML = text(value);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
function imageNode(src, className) {
|
function imageNode(src, className) {
|
||||||
if (!src) return null;
|
if (!src) return null;
|
||||||
var image = document.createElement("img");
|
var image = document.createElement("img");
|
||||||
@@ -179,7 +188,7 @@
|
|||||||
box.setAttribute("data-condition", "==");
|
box.setAttribute("data-condition", "==");
|
||||||
box.setAttribute("data-value", "card_pass");
|
box.setAttribute("data-value", "card_pass");
|
||||||
if (defaultMethod !== "card_pass") box.style.display = "none";
|
if (defaultMethod !== "card_pass") box.style.display = "none";
|
||||||
if (cardPass.desc) append(box, node("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc));
|
if (cardPass.desc) append(box, htmlNode("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc));
|
||||||
append(box, node("div", "muted-2-color em09 mb6", cardPass.only_password ? "请输入卡密" : "请输入卡号和密码"));
|
append(box, node("div", "muted-2-color em09 mb6", cardPass.only_password ? "请输入卡密" : "请输入卡号和密码"));
|
||||||
if (!cardPass.only_password) {
|
if (!cardPass.only_password) {
|
||||||
var cardInput = node("input", "form-control mb6");
|
var cardInput = node("input", "form-control mb6");
|
||||||
@@ -492,7 +501,7 @@
|
|||||||
+ '<i class="fa fas fa-angle-right em12 ml10" aria-hidden="true"></i>';
|
+ '<i class="fa fas fa-angle-right em12 ml10" aria-hidden="true"></i>';
|
||||||
append(products, pointsLink);
|
append(products, pointsLink);
|
||||||
}
|
}
|
||||||
if (catalog.more) append(products, node("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more));
|
if (catalog.more) append(products, htmlNode("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more));
|
||||||
renderMethods(products, methods, cardPass, wallet);
|
renderMethods(products, methods, cardPass, wallet);
|
||||||
var actionName = actionKey(action);
|
var actionName = actionKey(action);
|
||||||
var actionAllowed = allowed[actionName] === true;
|
var actionAllowed = allowed[actionName] === true;
|
||||||
|
|||||||
@@ -121,7 +121,9 @@ class PaymentRuntime {
|
|||||||
],
|
],
|
||||||
'vip' => [
|
'vip' => [
|
||||||
'enabled' => !empty($settings['pay_vip_pass_charge_s']),
|
'enabled' => !empty($settings['pay_vip_pass_charge_s']),
|
||||||
'desc' => sanitize_textarea_field((string) ($settings['pay_vip_pass_charge_desc'] ?? '')),
|
// 保存端已按 post 级 HTML 放行(schema sanitize_type html),
|
||||||
|
// 输出端不能再被 textarea 净化剥掉标签。
|
||||||
|
'desc' => wp_kses_post((string) ($settings['pay_vip_pass_charge_desc'] ?? '')),
|
||||||
// 默认非单密码(卡号+密码),对齐参考主题 pay_vip_pass_charge_only_password 默认 false。
|
// 默认非单密码(卡号+密码),对齐参考主题 pay_vip_pass_charge_only_password 默认 false。
|
||||||
'only_password' => !empty($settings['pay_vip_pass_charge_only_password']),
|
'only_password' => !empty($settings['pay_vip_pass_charge_only_password']),
|
||||||
],
|
],
|
||||||
@@ -218,7 +220,9 @@ class PaymentRuntime {
|
|||||||
'name' => $vip1_enabled ? $name1 : $badge,
|
'name' => $vip1_enabled ? $name1 : $badge,
|
||||||
'badge_text' => $badge,
|
'badge_text' => $badge,
|
||||||
'desc' => $desc,
|
'desc' => $desc,
|
||||||
'more' => wp_kses($more, $allowed_equity + ['div' => ['class' => true], 'p' => ['class' => true]]),
|
// 管理员编辑的富文本说明:与保存端同为 post 级 HTML(wp_kses_post),
|
||||||
|
// 不再用权益列表的窄白名单二次剥标签(h/img/hr/table 等)。
|
||||||
|
'more' => wp_kses_post($more),
|
||||||
'levels' => [
|
'levels' => [
|
||||||
1 => [
|
1 => [
|
||||||
'enabled' => $vip1_enabled,
|
'enabled' => $vip1_enabled,
|
||||||
|
|||||||
@@ -156,3 +156,19 @@ test('开通会员更多内容只在弹窗内渲染,不裸露在用户中心
|
|||||||
assert.match(modalSource, /'more'\s+=>/);
|
assert.match(modalSource, /'more'\s+=>/);
|
||||||
assert.match(payVipJs, /catalog\.more/);
|
assert.match(payVipJs, /catalog\.more/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('more 与卡密说明输出按 post 级 HTML 放行', () => {
|
||||||
|
// 保存端已 wp_kses_post,输出端不能再窄白名单/textarea 净化二次剥标签。
|
||||||
|
const vipSettings = slice(paymentRuntime, 'public static function get_payment_vip_settings', 'public static function get_payment_revenue_settings');
|
||||||
|
assert.match(vipSettings, /'more'\s+=> wp_kses_post\(\$more\)/);
|
||||||
|
assert.doesNotMatch(vipSettings, /\$allowed_equity \+ \['div'/);
|
||||||
|
const rechargeSettings = slice(paymentRuntime, 'public static function get_payment_recharge_settings', 'public static function get_payment_vip_settings');
|
||||||
|
assert.match(rechargeSettings, /'desc'\s+=> wp_kses_post\(\(string\) \(\$settings\['pay_vip_pass_charge_desc'\]/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('弹窗内 more 与卡密说明按 HTML 渲染而非纯文本', () => {
|
||||||
|
// node() 走 textContent 会把标签显示为字面文本;富文本字段必须走 innerHTML。
|
||||||
|
assert.match(payVipJs, /function htmlNode\(tag, className, value\) \{[\s\S]*?el\.innerHTML = text\(value\);/);
|
||||||
|
assert.ok(payVipJs.includes('htmlNode("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more)'));
|
||||||
|
assert.ok(payVipJs.includes('htmlNode("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc)'));
|
||||||
|
});
|
||||||
Reference in new issue
Block a user