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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user