fix(widgets): preserve badge HTML in icon card title/desc like reference theme
wp_kses_post() strips the custom <badge> element, so icon card title/desc lost admin-authored markup (e.g. <badge class="ml6 jb-blue">GO</badge>) both at save time and render time, diverging from the reference theme which stores and echoes these fields raw. - add shared inline whitelist (aksrt_icon_card_kses_tags/aksrt_icon_card_kses) and route both card renderers plus the media-brand fallback through it - drop the dead unused $allow array in aksrt_icon_cover_card - add 'rich_html' sanitize type (FieldSchema + SchemaProvider) and mark icon card title/desc fields in widget schemas, page display schema and the page config meta box save path - align header slider card module with reference show_widget_bg branches (aksrt-panel/flex1 vs padding-10 wrapper/relative-h) - update widget contract tests to pin the whitelist behavior
This commit is contained in:
@@ -7,6 +7,47 @@
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
if (!function_exists('aksrt_icon_card_kses_tags')) :
|
||||
/**
|
||||
* Inline HTML allow-list for icon card title/desc fields.
|
||||
*
|
||||
* legacy (Zibll) stores and echoes card title/desc raw, so admin-authored
|
||||
* markup such as `<badge class="ml6 jb-blue">GO <span class="fa ..."></span></badge>`
|
||||
* renders verbatim. `badge` is a custom element missing from WP's
|
||||
* `$allowedposttags`, so `wp_kses_post()` strips it; these fields must go
|
||||
* through this richer inline whitelist instead.
|
||||
*
|
||||
* @return array<string,array<string,bool>>
|
||||
*/
|
||||
function aksrt_icon_card_kses_tags(): array {
|
||||
return [
|
||||
'a' => ['href' => true, 'target' => true, 'rel' => true, 'class' => true],
|
||||
'badge' => ['class' => true],
|
||||
'span' => ['class' => true],
|
||||
'i' => ['class' => true, 'aria-hidden' => true],
|
||||
'em' => ['class' => true],
|
||||
'strong' => ['class' => true],
|
||||
'b' => ['class' => true],
|
||||
'small' => ['class' => true],
|
||||
'br' => [],
|
||||
];
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_icon_card_kses')) :
|
||||
/**
|
||||
* KSES with the shared inline whitelist; falls back to wp_kses_post()
|
||||
* when the tag helper is unavailable (e.g. file not yet loaded).
|
||||
*
|
||||
* @param string $value Raw admin-authored markup.
|
||||
* @return string
|
||||
*/
|
||||
function aksrt_icon_card_kses(string $value): string {
|
||||
$tags = aksrt_icon_card_kses_tags();
|
||||
return $tags ? wp_kses($value, $tags) : wp_kses_post($value);
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_card_icon_inner')) :
|
||||
/**
|
||||
* Resolve a card's stored `icon` value to renderable inner markup.
|
||||
@@ -154,9 +195,9 @@ if (!function_exists('aksrt_icon_card')) :
|
||||
$title = trim((string) $args['title']);
|
||||
$desc = trim((string) $args['desc']);
|
||||
if ($url !== '' && $title !== '') {
|
||||
$title_html = '<a class="main-color" href="' . esc_url($url) . '"' . $tgt . '>' . wp_kses_post($title) . '</a>';
|
||||
$title_html = '<a class="main-color" href="' . esc_url($url) . '"' . $tgt . '>' . aksrt_icon_card_kses($title) . '</a>';
|
||||
} else {
|
||||
$title_html = $title !== '' ? wp_kses_post($title) : '';
|
||||
$title_html = $title !== '' ? aksrt_icon_card_kses($title) : '';
|
||||
}
|
||||
|
||||
$body = '';
|
||||
@@ -164,7 +205,7 @@ if (!function_exists('aksrt_icon_card')) :
|
||||
$body .= '<div class="mt10 em12 text-ellipsis">' . $title_html . '</div>';
|
||||
}
|
||||
if ($desc !== '') {
|
||||
$body .= '<div class="muted-color mt6">' . wp_kses_post($desc) . '</div>';
|
||||
$body .= '<div class="muted-color mt6">' . aksrt_icon_card_kses($desc) . '</div>';
|
||||
}
|
||||
|
||||
$class = trim((string) $args['class'] . ' ' . (string) $args['type']);
|
||||
@@ -302,25 +343,17 @@ if (!function_exists('aksrt_icon_cover_card')) :
|
||||
}
|
||||
$icon = '<div class="' . esc_attr(trim($icon_class)) . '" style="' . esc_attr($style) . '">' . $icon_inner . '</div>';
|
||||
|
||||
// legacy allows HTML in title/desc (admin stores badges etc.).
|
||||
// legacy stores/echoes title/desc raw (admin authors badge markup
|
||||
// such as <badge class="ml6 jb-blue">GO</badge>), so these fields go
|
||||
// through the shared inline whitelist instead of wp_kses_post().
|
||||
$title = (string) $args['title'];
|
||||
$desc = (string) $args['desc'];
|
||||
$allow = [
|
||||
'badge' => ['class' => true],
|
||||
'span' => ['class' => true],
|
||||
'i' => ['class' => true, 'aria-hidden' => true],
|
||||
'em' => ['class' => true],
|
||||
'strong' => ['class' => true],
|
||||
'b' => [],
|
||||
'small' => ['class' => true],
|
||||
'br' => [],
|
||||
];
|
||||
$text = '';
|
||||
if (trim(wp_strip_all_tags($title)) !== '' || str_contains($title, '<')) {
|
||||
$text .= '<div class="em12 text-ellipsis font-bold"> ' . wp_kses_post($title) . '</div>';
|
||||
$text .= '<div class="em12 text-ellipsis font-bold"> ' . aksrt_icon_card_kses($title) . '</div>';
|
||||
}
|
||||
if (trim(wp_strip_all_tags($desc)) !== '' || str_contains($desc, '<')) {
|
||||
$text .= '<div class="muted-color mt6 text-ellipsis"> ' . wp_kses_post($desc) . '</div>';
|
||||
$text .= '<div class="muted-color mt6 text-ellipsis"> ' . aksrt_icon_card_kses($desc) . '</div>';
|
||||
}
|
||||
|
||||
$html = '';
|
||||
|
||||
@@ -421,24 +421,30 @@ if (!function_exists('aksrt_header_slide')) {
|
||||
$card_count = 0;
|
||||
if (aksrt_header_slider_device_is_visible($args['header_slider_card_s'] ?? [])) {
|
||||
$card_options = wp_parse_args((array) ($args['header_slider_card_option'] ?? []), [
|
||||
'icon_radius4' => false, 'cards' => [],
|
||||
'icon_radius4' => false, 'show_widget_bg' => false, 'cards' => [],
|
||||
]);
|
||||
// legacy (Zibll): with module background the card shrinks to
|
||||
// padding-10 inside a .zib-widget wrapper (.aksrt-panel here); the
|
||||
// bg-less variant stretches each card in a .flex1 column.
|
||||
$show_bg = !empty($card_options['show_widget_bg']);
|
||||
$items = '';
|
||||
foreach ((array) $card_options['cards'] as $card) {
|
||||
if (!is_array($card) || (empty($card['icon']) && empty($card['customize_icon']) && empty($card['custom_icon']) && empty($card['icon_image']))) {
|
||||
continue;
|
||||
}
|
||||
$card['class'] = $show_bg ? 'padding-10' : 'aksrt-panel';
|
||||
$card['icon_class'] = trim((string) ($card['icon_class'] ?? '') . (!empty($card_options['icon_radius4']) ? ' radius4' : ''));
|
||||
$inner = function_exists('aksrt_icon_cover_card') ? aksrt_icon_cover_card($card) : '';
|
||||
if ($inner === '') {
|
||||
continue;
|
||||
}
|
||||
$items .= '<div class="flex1">' . $inner . '</div>';
|
||||
$items .= '<div class="' . (!$show_bg ? 'flex1' : 'relative-h') . '">' . $inner . '</div>';
|
||||
$card_count++;
|
||||
}
|
||||
if ($items !== '') {
|
||||
$cards_html = '<div class="header-slider-card container">'
|
||||
. '<div class="flex jse flex-row gutters-5 flex-col-sm-2">' . $items . '</div></div>';
|
||||
. ($show_bg ? '<div class="aksrt-panel padding-10">' : '<div class="">')
|
||||
. '<div class="flex jse flex-row gutters-5 flex-col-sm-2">' . $items . '</div></div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,10 +55,10 @@ function aksrt_render_icon_cover_card(array $card): string {
|
||||
|
||||
$text_html = '<div class="icon-cover-desc ml10 flex1 px12-sm">';
|
||||
if ($title !== '') {
|
||||
$text_html .= '<div class="em12 text-ellipsis font-bold"> ' . wp_kses_post((string) $title) . '</div>';
|
||||
$text_html .= '<div class="em12 text-ellipsis font-bold"> ' . aksrt_icon_card_kses((string) $title) . '</div>';
|
||||
}
|
||||
if ($desc !== '') {
|
||||
$text_html .= '<div class="muted-color mt6 text-ellipsis"> ' . wp_kses_post((string) $desc) . '</div>';
|
||||
$text_html .= '<div class="muted-color mt6 text-ellipsis"> ' . aksrt_icon_card_kses((string) $desc) . '</div>';
|
||||
}
|
||||
$text_html .= '</div>';
|
||||
|
||||
|
||||
@@ -250,8 +250,10 @@ final class PageConfigMetaBox {
|
||||
}
|
||||
$link = is_array($card['link'] ?? null) ? $card['link'] : [];
|
||||
$cards[] = [
|
||||
'title' => wp_kses_post((string) ($card['title'] ?? '')),
|
||||
'desc' => wp_kses_post((string) ($card['desc'] ?? '')),
|
||||
// legacy echoes card title/desc raw (admin authors <badge> etc.);
|
||||
// wp_kses_post() would strip the custom badge element.
|
||||
'title' => aksrt_icon_card_kses((string) ($card['title'] ?? '')),
|
||||
'desc' => aksrt_icon_card_kses((string) ($card['desc'] ?? '')),
|
||||
// Icon picker values may carry an inline "customsvg-<svg>" payload;
|
||||
// sanitize_text_field would strip it down to a bare prefix.
|
||||
'icon' => function_exists('aksrt_sanitize_icon_value')
|
||||
|
||||
@@ -181,7 +181,7 @@ class SchemaPageDisplay {
|
||||
}
|
||||
|
||||
private static function card_fields(): array {
|
||||
return [self::field('icon_radius4', '方形图标', 'switcher', false), self::field('cards', '添加卡片', 'group', [], ['button_title' => '添加内容', 'fields' => [self::field('title', '文案标题', 'textarea', '', ['sanitize_type' => 'html']), self::field('desc', '文案简介', 'textarea', '', ['sanitize_type' => 'html']), self::field('icon', '选择图标', 'icon', 'fa fas fa-magic'), self::field('customize_icon', '自定义图标', 'textarea', '', ['sanitize_type' => 'html']), self::field('icon_class', '图标颜色', 'palette', 'c-yellow', ['options' => self::aksrt_palette(true)]), self::field('link', '跳转链接', 'link', [])]])];
|
||||
return [self::field('icon_radius4', '方形图标', 'switcher', false), self::field('cards', '添加卡片', 'group', [], ['button_title' => '添加内容', 'fields' => [self::field('title', '文案标题', 'textarea', '', ['sanitize_type' => 'rich_html']), self::field('desc', '文案简介', 'textarea', '', ['sanitize_type' => 'rich_html']), self::field('icon', '选择图标', 'icon', 'fa fas fa-magic'), self::field('customize_icon', '自定义图标', 'textarea', '', ['sanitize_type' => 'html']), self::field('icon_class', '图标颜色', 'palette', 'c-yellow', ['options' => self::aksrt_palette(true)]), self::field('link', '跳转链接', 'link', [])]])];
|
||||
}
|
||||
|
||||
private static function orderby_fields(): array { return [self::field('lists', '显示排序方式', 'select', ['modified', 'views', 'like', 'comment_count'], ['multiple' => true, 'sortable' => true, 'options' => self::post_order_options(false)]), self::field('dropdown', '更多排序方式', 'switcher', false)]; }
|
||||
|
||||
@@ -821,6 +821,13 @@ class SchemaProvider {
|
||||
return array_values(array_unique($ids));
|
||||
case 'html':
|
||||
return wp_kses_post((string) $value);
|
||||
case 'rich_html':
|
||||
// Inline whitelist for card title/desc: `badge` is a custom
|
||||
// element that wp_kses_post() would strip, but legacy (Zibll)
|
||||
// renders such markup verbatim.
|
||||
return function_exists('aksrt_icon_card_kses')
|
||||
? aksrt_icon_card_kses((string) $value)
|
||||
: wp_kses_post((string) $value);
|
||||
case 'textarea':
|
||||
return sanitize_textarea_field((string) $value);
|
||||
case 'code':
|
||||
|
||||
@@ -85,12 +85,18 @@ final class FieldSchema {
|
||||
? aksrt_sanitize_icon_value((string) $value)
|
||||
: sanitize_text_field((string) $value);
|
||||
case 'textarea':
|
||||
if (($field['sanitize'] ?? '') === 'rich_html') {
|
||||
return self::rich_inline_html((string) $value);
|
||||
}
|
||||
return !empty($field['allow_html']) ? wp_kses_post((string) $value) : sanitize_textarea_field((string) $value);
|
||||
case 'text':
|
||||
default:
|
||||
if (($field['sanitize'] ?? '') === 'url') {
|
||||
return esc_url_raw((string) $value);
|
||||
}
|
||||
if (($field['sanitize'] ?? '') === 'rich_html') {
|
||||
return self::rich_inline_html((string) $value);
|
||||
}
|
||||
if (($field['sanitize'] ?? '') === 'ids') {
|
||||
return implode(',', array_values(array_unique(array_filter(array_map('absint', preg_split('/[,,\s]+/', (string) $value))))));
|
||||
}
|
||||
@@ -103,6 +109,17 @@ final class FieldSchema {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline rich-HTML sanitizer for admin-authored card title/desc fields.
|
||||
* `badge` and friends are not in WP's $allowedposttags, so wp_kses_post()
|
||||
* would strip the exact markup legacy/Zibll renders verbatim.
|
||||
*/
|
||||
private static function rich_inline_html(string $value): string {
|
||||
return function_exists('aksrt_icon_card_kses')
|
||||
? aksrt_icon_card_kses($value)
|
||||
: wp_kses_post($value);
|
||||
}
|
||||
|
||||
private static function empty_value(string $type) {
|
||||
if ($type === 'switcher') {
|
||||
return false;
|
||||
|
||||
@@ -537,8 +537,8 @@ final class WidgetSchemas {
|
||||
['id' => 'icon_class', 'type' => 'palette', 'label' => '图标样式', 'default' => 'c-yellow', 'options' => self::icon_background_palette(), 'dependency' => ['icon_radius', '!=', '']],
|
||||
['id' => 'icon_color', 'type' => 'palette', 'label' => '图标颜色', 'default' => 'key-color', 'options' => self::icon_text_palette(), 'dependency' => ['icon_radius|icon_custom_color', '==|==', '|']],
|
||||
['id' => 'icon_custom_color', 'type' => 'color', 'label' => '自定义图标颜色', 'default' => '', 'dependency' => ['icon_radius', '==', '']],
|
||||
['id' => 'title', 'type' => 'textarea', 'label' => '文案标题', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 1]],
|
||||
['id' => 'desc', 'type' => 'textarea', 'label' => '文案简介', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 2]],
|
||||
['id' => 'title', 'type' => 'textarea', 'label' => '文案标题', 'default' => '', 'allow_html' => true, 'sanitize' => 'rich_html', 'attributes' => ['rows' => 1]],
|
||||
['id' => 'desc', 'type' => 'textarea', 'label' => '文案简介', 'default' => '', 'allow_html' => true, 'sanitize' => 'rich_html', 'attributes' => ['rows' => 2]],
|
||||
]],
|
||||
];
|
||||
}
|
||||
@@ -550,8 +550,8 @@ final class WidgetSchemas {
|
||||
['id' => 'show_widget_bg', 'type' => 'switcher', 'label' => '显示模块背景', 'default' => false],
|
||||
['id' => 'icon_radius4', 'type' => 'switcher', 'label' => '方形图标', 'default' => false],
|
||||
['id' => 'cards', 'type' => 'group', 'label' => '添加卡片', 'max_items' => 40, 'default' => [], 'fields' => [
|
||||
['id' => 'title', 'type' => 'textarea', 'label' => '文案标题', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 1]],
|
||||
['id' => 'desc', 'type' => 'textarea', 'label' => '文案简介', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 2]],
|
||||
['id' => 'title', 'type' => 'textarea', 'label' => '文案标题', 'default' => '', 'allow_html' => true, 'sanitize' => 'rich_html', 'attributes' => ['rows' => 1]],
|
||||
['id' => 'desc', 'type' => 'textarea', 'label' => '文案简介', 'default' => '', 'allow_html' => true, 'sanitize' => 'rich_html', 'attributes' => ['rows' => 2]],
|
||||
['id' => 'icon', 'type' => 'icon', 'label' => '选择图标', 'default' => 'fa fas fa-magic'],
|
||||
['id' => 'customize_icon', 'type' => 'textarea', 'label' => '自定义图标', 'default' => '', 'allow_html' => true, 'attributes' => ['rows' => 1]],
|
||||
['id' => 'icon_class', 'type' => 'palette', 'label' => '图标颜色', 'default' => 'c-yellow', 'options' => self::icon_background_palette()],
|
||||
|
||||
@@ -42,8 +42,15 @@ test('icon card widgets expose Zibll vertical and horizontal contracts', () => {
|
||||
for (const token of ['clearfix', 'row gutters-5', 'icon-card', 'icon-cover-card', 'icon-cover-icon', 'icon-cover-desc']) {
|
||||
assert.match(renderer + helper, new RegExp(token.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')));
|
||||
}
|
||||
assert.match(helper, /wp_kses_post\(\$title\)/);
|
||||
assert.match(helper, /wp_kses_post\(\$desc\)/);
|
||||
// Card title/desc must keep legacy badge markup: fields go through the
|
||||
// shared inline whitelist (aksrt_icon_card_kses), never bare wp_kses_post
|
||||
// which strips the custom <badge> element.
|
||||
assert.match(helper, /function aksrt_icon_card_kses_tags/);
|
||||
assert.match(helper, /'badge'\s+=>\s*\['class' => true\]/);
|
||||
assert.match(helper, /aksrt_icon_card_kses\(\$title\)/);
|
||||
assert.match(helper, /aksrt_icon_card_kses\(\$desc\)/);
|
||||
assert.doesNotMatch(helper, /wp_kses_post\(\$title\)/);
|
||||
assert.doesNotMatch(helper, /wp_kses_post\(\$desc\)/);
|
||||
});
|
||||
|
||||
test('button group uses the complete Zibll schema, DOM and responsive sizing contract', () => {
|
||||
|
||||
Reference in New Issue
Block a user