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:
kate522 committed 2026-09-19 16:39:59 +08:00
1 parent 2f571c2a98
commit cd7082454a
9 files changed
+108 -36

No files matched your search

+49 -16
View File
@@ -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 = '';
+9 -3
View File
@@ -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>';
}
}
+8 -8
View File
@@ -53,14 +53,14 @@ function aksrt_render_icon_cover_card(array $card): string {
$icon_container = '<div class="icon-cover-icon badg cir ' . esc_attr($icon_class) . '" style="font-size: 25px;">' . $icon_html . '</div>';
$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>';
}
if ($desc !== '') {
$text_html .= '<div class="muted-color mt6 text-ellipsis"> ' . wp_kses_post((string) $desc) . '</div>';
}
$text_html .= '</div>';
$text_html = '<div class="icon-cover-desc ml10 flex1 px12-sm">';
if ($title !== '') {
$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"> ' . aksrt_icon_card_kses((string) $desc) . '</div>';
}
$text_html .= '</div>';
$card_inner = $icon_container . $text_html;
$cls = 'icon-cover-card flex ac ' . esc_attr((string) ($card['class'] ?? 'aksrt-panel'));