feat: product page layout toggle (flat/card), fix email shortcode rendering

- Add layout_style setting (flat default) to product appearance
  - AppearanceController: sanitize & persist layout_style, add to default settings
  - Admin SPA: Layout Style select in Appearance > Product
  - Customer SPA: useEffect targets <main> bg-white in flat mode (full-width),
    card mode uses per-section white floating cards on gray background
  - Accordion sections styled per mode: flat=border-t dividers, card=white cards

- Fix email shortcode gaps (EmailRenderer, EmailManager)
  - Add missing variables: return_url, contact_url, account_url (alias),
    payment_error_reason, order_items_list (alias for order_items_table)
  - Fix customer_note extra_data key mismatch (note → customer_note)
  - Pass low_stock_threshold via extra_data in low_stock email send
This commit is contained in:
Dwindi Ramadhana
2026-03-04 01:14:56 +07:00
parent 7ff429502d
commit 90169b508d
46 changed files with 2337 additions and 1278 deletions

View File

@@ -46,6 +46,18 @@ class ProductsController
return trim($sanitized);
}
/**
* Sanitize rich text (allows HTML tags)
*/
private static function sanitize_rich_text($value)
{
if (!isset($value) || $value === '') {
return '';
}
$sanitized = wp_kses_post($value);
return trim($sanitized);
}
/**
* Sanitize numeric value
*/
@@ -335,8 +347,12 @@ class ProductsController
$product->set_slug(self::sanitize_slug($data['slug']));
}
$product->set_status(sanitize_key($data['status'] ?? 'publish'));
$product->set_description(self::sanitize_textarea($data['description'] ?? ''));
$product->set_short_description(self::sanitize_textarea($data['short_description'] ?? ''));
if (isset($data['description'])) {
$product->set_description(self::sanitize_rich_text($data['description'] ?? ''));
}
if (isset($data['short_description'])) {
$product->set_short_description(self::sanitize_textarea($data['short_description'] ?? ''));
}
if (!empty($data['sku'])) {
$product->set_sku(self::sanitize_text($data['sku']));
@@ -489,7 +505,7 @@ class ProductsController
if (isset($data['name'])) $product->set_name(self::sanitize_text($data['name']));
if (isset($data['slug'])) $product->set_slug(self::sanitize_slug($data['slug']));
if (isset($data['status'])) $product->set_status(sanitize_key($data['status']));
if (isset($data['description'])) $product->set_description(self::sanitize_textarea($data['description']));
if (isset($data['description'])) $product->set_description(self::sanitize_rich_text($data['description']));
if (isset($data['short_description'])) $product->set_short_description(self::sanitize_textarea($data['short_description']));
if (isset($data['sku'])) $product->set_sku(self::sanitize_text($data['sku']));
@@ -942,10 +958,17 @@ class ProductsController
$value = $term ? $term->name : $value;
}
} else {
// Custom attribute - stored as lowercase in meta
$meta_key = 'attribute_' . strtolower($attr_name);
// Custom attribute - stored as sanitize_title in meta
$sanitized_name = sanitize_title($attr_name);
$meta_key = 'attribute_' . $sanitized_name;
$value = get_post_meta($variation_id, $meta_key, true);
// Fallback to legacy lowercase if not found
if ($value === '') {
$meta_key_legacy = 'attribute_' . strtolower($attr_name);
$value = get_post_meta($variation_id, $meta_key_legacy, true);
}
// Capitalize the attribute name for display to match admin SPA
$clean_name = ucfirst($attr_name);
}
@@ -1029,8 +1052,27 @@ class ProductsController
foreach ($parent_attributes as $attr_name => $parent_attr) {
if (!$parent_attr->get_variation()) continue;
if (strcasecmp($display_name, $attr_name) === 0 || strcasecmp($display_name, ucfirst($attr_name)) === 0) {
$wc_attributes[strtolower($attr_name)] = strtolower($value);
$is_match = false;
if (strpos($attr_name, 'pa_') === 0) {
$label = wc_attribute_label($attr_name);
if (strcasecmp($display_name, $label) === 0 || strcasecmp($display_name, $attr_name) === 0) {
$is_match = true;
}
} else {
// Custom attribute: Check exact name, or sanitized version
if (
strcasecmp($display_name, $attr_name) === 0 ||
strcasecmp($display_name, $parent_attr->get_name()) === 0 ||
sanitize_title($display_name) === sanitize_title($attr_name)
) {
$is_match = true;
}
}
if ($is_match) {
// WooCommerce expects the exact attribute slug as the key
$wc_attributes[$attr_name] = $value;
break;
}
}
@@ -1095,7 +1137,7 @@ class ProductsController
global $wpdb;
foreach ($wc_attributes as $attr_name => $attr_value) {
$meta_key = 'attribute_' . $attr_name;
$meta_key = 'attribute_' . sanitize_title($attr_name);
$wpdb->delete(
$wpdb->postmeta,