2026-02-28 11:15:59 +01:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* WooCommerce Twig Extension.
|
|
|
|
|
*
|
|
|
|
|
* Registers all WooCommerce and WordPress functions/filters needed
|
|
|
|
|
* by the child theme's Twig templates.
|
|
|
|
|
*
|
|
|
|
|
* @package WcBootstrap
|
|
|
|
|
* @since 0.1.0
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
namespace WcBootstrap\Twig;
|
|
|
|
|
|
|
|
|
|
use Twig\Extension\AbstractExtension;
|
|
|
|
|
use Twig\TwigFunction;
|
|
|
|
|
use Twig\TwigFilter;
|
|
|
|
|
|
|
|
|
|
class WooCommerceExtension extends AbstractExtension {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* {@inheritdoc}
|
|
|
|
|
*/
|
|
|
|
|
public function getFunctions(): array {
|
|
|
|
|
return array_merge(
|
|
|
|
|
$this->getWordPressFunctions(),
|
|
|
|
|
$this->getWooCommerceFunctions()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* {@inheritdoc}
|
|
|
|
|
*/
|
|
|
|
|
public function getFilters(): array {
|
|
|
|
|
return [
|
|
|
|
|
// Escaping filters (parent registers as functions only, templates use as |filter).
|
|
|
|
|
new TwigFilter( 'esc_html', 'esc_html', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFilter( 'esc_attr', 'esc_attr', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFilter( 'esc_url', 'esc_url', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
|
|
|
|
|
// Text processing filters.
|
|
|
|
|
new TwigFilter( 'wpautop', 'wpautop', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFilter( 'wp_kses_post', 'wp_kses_post', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFilter( 'wptexturize', 'wptexturize' ),
|
|
|
|
|
new TwigFilter( 'do_shortcode', 'do_shortcode', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* WordPress core functions not registered by the parent theme's TwigService.
|
|
|
|
|
*
|
|
|
|
|
* @return TwigFunction[]
|
|
|
|
|
*/
|
|
|
|
|
private function getWordPressFunctions(): array {
|
|
|
|
|
return [
|
|
|
|
|
// Hook system.
|
|
|
|
|
new TwigFunction( 'do_action', [ $this, 'doAction' ], [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'apply_filters', [ $this, 'applyFilters' ] ),
|
|
|
|
|
|
|
|
|
|
// Security.
|
|
|
|
|
new TwigFunction( 'wp_nonce_field', function ( string $action = '-1', string $name = '_wpnonce', bool $referer = true ): string {
|
|
|
|
|
return wp_nonce_field( $action, $name, $referer, false );
|
|
|
|
|
}, [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
|
|
|
|
|
// Options and settings.
|
|
|
|
|
new TwigFunction( 'get_option', 'get_option' ),
|
|
|
|
|
|
|
|
|
|
// User functions.
|
|
|
|
|
new TwigFunction( 'get_current_user_id', 'get_current_user_id' ),
|
|
|
|
|
new TwigFunction( 'is_user_logged_in', 'is_user_logged_in' ),
|
|
|
|
|
|
|
|
|
|
// URL helpers.
|
|
|
|
|
new TwigFunction( 'wp_lostpassword_url', 'wp_lostpassword_url' ),
|
|
|
|
|
new TwigFunction( 'get_permalink', 'get_permalink' ),
|
|
|
|
|
new TwigFunction( 'get_term_link', 'get_term_link' ),
|
|
|
|
|
new TwigFunction( 'wp_get_attachment_url', 'wp_get_attachment_url' ),
|
|
|
|
|
|
|
|
|
|
// Content rendering (echo-based, wrapped in ob_start).
|
|
|
|
|
new TwigFunction( 'get_avatar', 'get_avatar', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'the_title', function (): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
the_title();
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}, [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'the_content', function (): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
the_content();
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}, [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
|
|
|
|
|
// Taxonomy.
|
|
|
|
|
new TwigFunction( 'term_description', 'term_description', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'single_term_title', function ( string $prefix = '' ): string {
|
|
|
|
|
return single_term_title( $prefix, false );
|
|
|
|
|
} ),
|
|
|
|
|
|
|
|
|
|
// Date/time.
|
|
|
|
|
new TwigFunction( 'date_i18n', 'date_i18n' ),
|
|
|
|
|
|
|
|
|
|
// Text processing (as functions).
|
|
|
|
|
new TwigFunction( 'wpautop', 'wpautop', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'wptexturize', 'wptexturize' ),
|
|
|
|
|
new TwigFunction( 'wp_parse_url', 'wp_parse_url' ),
|
|
|
|
|
|
|
|
|
|
// Formatting.
|
|
|
|
|
new TwigFunction( 'sprintf', 'sprintf' ),
|
|
|
|
|
|
|
|
|
|
// Dynamic function calls.
|
|
|
|
|
new TwigFunction( 'call_user_func', [ $this, 'callUserFunc' ], [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'fn', [ $this, 'callFunction' ] ),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* WooCommerce-specific functions.
|
|
|
|
|
*
|
|
|
|
|
* @return TwigFunction[]
|
|
|
|
|
*/
|
|
|
|
|
private function getWooCommerceFunctions(): array {
|
|
|
|
|
return [
|
|
|
|
|
// URL functions.
|
|
|
|
|
new TwigFunction( 'wc_get_cart_url', 'wc_get_cart_url' ),
|
|
|
|
|
new TwigFunction( 'wc_get_checkout_url', 'wc_get_checkout_url' ),
|
|
|
|
|
new TwigFunction( 'wc_get_page_permalink', 'wc_get_page_permalink' ),
|
|
|
|
|
new TwigFunction( 'wc_get_endpoint_url', 'wc_get_endpoint_url' ),
|
|
|
|
|
new TwigFunction( 'wc_get_account_endpoint_url', 'wc_get_account_endpoint_url' ),
|
|
|
|
|
new TwigFunction( 'wc_logout_url', 'wc_logout_url' ),
|
|
|
|
|
|
|
|
|
|
// Boolean helpers.
|
|
|
|
|
new TwigFunction( 'wc_coupons_enabled', 'wc_coupons_enabled' ),
|
|
|
|
|
new TwigFunction( 'wc_shipping_enabled', 'wc_shipping_enabled' ),
|
|
|
|
|
new TwigFunction( 'wc_ship_to_billing_address_only', 'wc_ship_to_billing_address_only' ),
|
|
|
|
|
new TwigFunction( 'wc_terms_and_conditions_checkbox_enabled', 'wc_terms_and_conditions_checkbox_enabled' ),
|
|
|
|
|
|
|
|
|
|
// Order functions.
|
|
|
|
|
new TwigFunction( 'wc_get_order', 'wc_get_order' ),
|
|
|
|
|
new TwigFunction( 'wc_get_order_status_name', 'wc_get_order_status_name' ),
|
|
|
|
|
new TwigFunction( 'wc_format_datetime', 'wc_format_datetime' ),
|
|
|
|
|
new TwigFunction( 'wc_date_format', 'wc_date_format' ),
|
|
|
|
|
|
|
|
|
|
// Product functions.
|
|
|
|
|
new TwigFunction( 'wc_attribute_label', 'wc_attribute_label' ),
|
|
|
|
|
new TwigFunction( 'wc_placeholder_img_src', 'wc_placeholder_img_src' ),
|
|
|
|
|
new TwigFunction( 'wc_get_image_size', 'wc_get_image_size' ),
|
|
|
|
|
|
|
|
|
|
// Account functions.
|
|
|
|
|
new TwigFunction( 'wc_get_account_menu_items', 'wc_get_account_menu_items' ),
|
|
|
|
|
new TwigFunction( 'wc_get_account_menu_item_classes', 'wc_get_account_menu_item_classes' ),
|
|
|
|
|
new TwigFunction( 'wc_get_account_orders_columns', 'wc_get_account_orders_columns' ),
|
|
|
|
|
new TwigFunction( 'wc_get_account_orders_actions', 'wc_get_account_orders_actions' ),
|
|
|
|
|
new TwigFunction( 'wc_get_account_payment_methods_columns', 'wc_get_account_payment_methods_columns' ),
|
|
|
|
|
new TwigFunction( 'wc_get_account_formatted_address', 'wc_get_account_formatted_address', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'wc_get_customer_saved_methods_list', 'wc_get_customer_saved_methods_list' ),
|
|
|
|
|
new TwigFunction( 'wc_get_credit_card_type_label', 'wc_get_credit_card_type_label' ),
|
|
|
|
|
|
|
|
|
|
// Content/form functions (echo-based, need output capture).
|
|
|
|
|
new TwigFunction( 'wc_print_notices', [ $this, 'wcPrintNotices' ], [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'wc_display_item_meta', [ $this, 'wcDisplayItemMeta' ], [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'wc_query_string_form_fields', [ $this, 'wcQueryStringFormFields' ], [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'woocommerce_form_field', [ $this, 'woocommerceFormField' ], [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'woocommerce_breadcrumb', function ( array $args = [] ): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
woocommerce_breadcrumb( $args );
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}, [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'woocommerce_quantity_input', function ( array $args = [], $product = null ): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
woocommerce_quantity_input( $args, $product, true );
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}, [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'woocommerce_product_loop_start', function (): string {
|
|
|
|
|
return woocommerce_product_loop_start( false );
|
|
|
|
|
}, [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'woocommerce_product_loop_end', function (): string {
|
|
|
|
|
return woocommerce_product_loop_end( false );
|
|
|
|
|
}, [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
|
|
|
|
|
// Text/policy functions.
|
|
|
|
|
new TwigFunction( 'wc_terms_and_conditions_checkbox_text', 'wc_terms_and_conditions_checkbox_text', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'wc_replace_policy_page_link_placeholders', 'wc_replace_policy_page_link_placeholders', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
new TwigFunction( 'wc_get_privacy_policy_text', 'wc_get_privacy_policy_text', [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
|
|
|
|
|
// Data helpers.
|
|
|
|
|
new TwigFunction( 'wc_get_post_data_by_key', 'wc_get_post_data_by_key' ),
|
|
|
|
|
new TwigFunction( 'wc_get_page_id', 'wc_get_page_id' ),
|
|
|
|
|
|
|
|
|
|
// Template recursion (renders a WC template and returns HTML).
|
|
|
|
|
new TwigFunction( 'wc_get_template', [ $this, 'wcGetTemplate' ], [ 'is_safe' => [ 'html' ] ] ),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
// Method implementations for functions that need special handling.
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Execute a WordPress action hook and capture its output.
|
|
|
|
|
*
|
|
|
|
|
* Actions may echo HTML (e.g., rendering sub-templates). Output buffering
|
|
|
|
|
* captures this so it can be returned as a Twig-safe string.
|
|
|
|
|
*
|
|
|
|
|
* @param string $tag Action hook name.
|
|
|
|
|
* @param mixed ...$args Arguments to pass to the hook.
|
|
|
|
|
* @return string Captured HTML output.
|
|
|
|
|
*/
|
|
|
|
|
public function doAction( string $tag, ...$args ): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
do_action( $tag, ...$args );
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Apply WordPress filters.
|
|
|
|
|
*
|
|
|
|
|
* @param string $tag Filter hook name.
|
|
|
|
|
* @param mixed ...$args Arguments (first is the value to filter).
|
|
|
|
|
* @return mixed Filtered value.
|
|
|
|
|
*/
|
|
|
|
|
public function applyFilters( string $tag, ...$args ): mixed {
|
|
|
|
|
return apply_filters( $tag, ...$args );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Call a user function and capture its output.
|
|
|
|
|
*
|
|
|
|
|
* Used for tab callbacks that echo content.
|
|
|
|
|
*
|
|
|
|
|
* @param callable $callback Function to call.
|
|
|
|
|
* @param mixed ...$args Arguments.
|
|
|
|
|
* @return string Captured output.
|
|
|
|
|
*/
|
|
|
|
|
public function callUserFunc( callable $callback, ...$args ): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
call_user_func( $callback, ...$args );
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-01 01:02:43 +01:00
|
|
|
* Allowlist of PHP functions that can be called via fn() in Twig templates.
|
|
|
|
|
*
|
|
|
|
|
* Prevents arbitrary function execution (e.g., exec, system) if template
|
|
|
|
|
* context were ever compromised. Only functions actually used in templates
|
|
|
|
|
* are permitted.
|
|
|
|
|
*/
|
|
|
|
|
private const ALLOWED_FUNCTIONS = [
|
|
|
|
|
'WC',
|
|
|
|
|
'_n',
|
|
|
|
|
'get_pagenum_link',
|
|
|
|
|
'wc_review_ratings_enabled',
|
|
|
|
|
'wc_get_product_category_list',
|
|
|
|
|
'wc_get_product_tag_list',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Call a whitelisted PHP function by name and return its result.
|
2026-02-28 11:15:59 +01:00
|
|
|
*
|
|
|
|
|
* Enables `fn('WC')` in templates to access the WooCommerce singleton
|
|
|
|
|
* and chain method calls via Twig's property accessor.
|
|
|
|
|
*
|
2026-03-01 01:02:43 +01:00
|
|
|
* Only functions in the ALLOWED_FUNCTIONS list can be called. This prevents
|
|
|
|
|
* arbitrary code execution if template context were ever compromised.
|
|
|
|
|
*
|
2026-02-28 11:15:59 +01:00
|
|
|
* @param string $name Function name.
|
|
|
|
|
* @param mixed ...$args Arguments.
|
|
|
|
|
* @return mixed Function return value.
|
|
|
|
|
*
|
2026-03-01 01:02:43 +01:00
|
|
|
* @throws \RuntimeException If function is not allowed or does not exist.
|
2026-02-28 11:15:59 +01:00
|
|
|
*/
|
|
|
|
|
public function callFunction( string $name, ...$args ): mixed {
|
2026-03-01 01:02:43 +01:00
|
|
|
if ( ! in_array( $name, self::ALLOWED_FUNCTIONS, true ) ) {
|
|
|
|
|
throw new \RuntimeException( "Function {$name} is not allowed. Add it to ALLOWED_FUNCTIONS." );
|
|
|
|
|
}
|
2026-02-28 11:15:59 +01:00
|
|
|
if ( ! function_exists( $name ) ) {
|
|
|
|
|
throw new \RuntimeException( "Function {$name} does not exist." );
|
|
|
|
|
}
|
|
|
|
|
return $name( ...$args );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Capture wc_print_notices() output.
|
|
|
|
|
*
|
|
|
|
|
* @return string Rendered notices HTML.
|
|
|
|
|
*/
|
|
|
|
|
public function wcPrintNotices(): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
wc_print_notices();
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render item meta without echoing.
|
|
|
|
|
*
|
|
|
|
|
* @param mixed $item WC_Order_Item object.
|
|
|
|
|
* @param array $args Display arguments.
|
|
|
|
|
* @return string Rendered meta HTML.
|
|
|
|
|
*/
|
|
|
|
|
public function wcDisplayItemMeta( $item, array $args = [] ): string {
|
|
|
|
|
$args['echo'] = false;
|
|
|
|
|
return wc_display_item_meta( $item, $args );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render query string form fields and return HTML.
|
|
|
|
|
*
|
|
|
|
|
* @param mixed $values Values array or null.
|
|
|
|
|
* @param array $exclude Keys to exclude.
|
|
|
|
|
* @param string $currentKey Current key prefix.
|
|
|
|
|
* @return string Hidden input fields HTML.
|
|
|
|
|
*/
|
|
|
|
|
public function wcQueryStringFormFields( $values = null, array $exclude = [], string $currentKey = '' ): string {
|
|
|
|
|
return wc_query_string_form_fields( $values, $exclude, $currentKey, true );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render a WooCommerce form field and return HTML.
|
|
|
|
|
*
|
|
|
|
|
* @param string $key Field key.
|
|
|
|
|
* @param array $args Field arguments.
|
|
|
|
|
* @param mixed $value Field value.
|
|
|
|
|
* @return string Rendered field HTML.
|
|
|
|
|
*/
|
|
|
|
|
public function woocommerceFormField( string $key, array $args, $value = null ): string {
|
|
|
|
|
$args['return'] = true;
|
|
|
|
|
return woocommerce_form_field( $key, $args, $value );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Render a WooCommerce template and return its HTML.
|
|
|
|
|
*
|
|
|
|
|
* Enables recursive template calls from within Twig templates.
|
|
|
|
|
* The rendered template goes through the same interception pipeline,
|
|
|
|
|
* so it will use the Twig version if one exists.
|
|
|
|
|
*
|
|
|
|
|
* @param string $templateName Template name (e.g., 'order/order-downloads.php').
|
|
|
|
|
* @param array $args Template context variables.
|
|
|
|
|
* @return string Rendered HTML.
|
|
|
|
|
*/
|
|
|
|
|
public function wcGetTemplate( string $templateName, array $args = [] ): string {
|
|
|
|
|
ob_start();
|
|
|
|
|
wc_get_template( $templateName, $args );
|
|
|
|
|
return ob_get_clean();
|
|
|
|
|
}
|
|
|
|
|
}
|