&/\[\]\x00-\x20=]++)@', $shortcode, $matches ); $matched_shortcodes = $matches[1]; foreach ( $matched_shortcodes as $shortcode ) { // If a shortcode exists that is not a default shortcode, don't replace content. The user has already built a custom page. if ( ! in_array( $shortcode, $default_shortcodes, true ) ) { $should_replace_content = false; break; } } return $should_replace_content; } /** * Stop redirecting to Cart page when enabling builder on Checkout page. * * @since 4.14.0 * * @link https://github.com/elegantthemes/Divi/issues/23873 * * @param bool $flag Flag. * * @return bool */ function et_builder_stop_cart_redirect_while_enabling_builder( $flag ) { /* * Don't need to check if the current page is Checkout page since this filter * `woocommerce_checkout_redirect_empty_cart` only fires if the * current page is a Checkout page. */ $post_id = get_the_ID(); if ( is_array( $_GET ) && isset( $_GET['et_fb'] ) && '1' === $_GET['et_fb'] ) { $is_builder_activation_request = true; } else { // Verify if the request is a valid Builder activation request. $is_builder_activation_request = et_core_security_check( '', "et_fb_activation_nonce_{$post_id}", 'et_fb_activation_nonce', '_REQUEST', false ); } return $is_builder_activation_request ? false : $flag; } /** * Message to be displayed in Checkout Payment Info module in VB mode. * * So styling the Notice becomes easier. * * @since 4.14.0 * * @return string */ function et_builder_wc_no_available_payment_methods_message() { // Fallback. $message = esc_html__( 'Sorry, it seems that there are no available payment methods for your state. Please contact us if you require assistance or wish to make alternate arrangements.' ); if ( ! function_exists( 'WC' ) ) { return $message; } if ( ! isset( WC()->customer ) && ! method_exists( WC()->customer, 'get_billing_country' ) ) { return $message; } $message = WC()->customer->get_billing_country() ? esc_html__( 'Sorry, it seems that there are no available payment methods for your state. Please contact us if you require assistance or wish to make alternate arrangements.', 'et_builder' ) : esc_html__( 'Please fill in your details above to see available payment methods.', 'et_builder' ); return apply_filters( 'woocommerce_no_available_payment_methods_message', $message ); } /** * Output the cart shipping calculator. * * @param string $button_text Text for the shipping calculation toggle. */ function et_builder_woocommerce_shipping_calculator( $button_text = '' ) { wp_enqueue_script( 'wc-country-select' ); wc_get_template( 'cart/shipping-calculator.php', array( 'button_text' => $button_text, ) ); } /** * Gets the Checkout modules notice to be displayed on non-checkout pages. * * @since 4.14.0 * * @used-by et_fb_get_static_backend_helpers() * * @return string */ function et_builder_wc_get_non_checkout_page_notice() { return esc_html__( 'This module will not function properly on the front end of your website because this is not the assigned Checkout page.', 'et_builder' ); } /** * Gets the Checkout notice to be displayed on Checkout Payment Info module. * * @since 4.14.0 * * @param string $woocommerce_ship_to_destination Default `shipping`. * * @used-by et_fb_get_static_backend_helpers() * * @return string */ function et_builder_wc_get_checkout_notice( $woocommerce_ship_to_destination = 'shipping' ) { $settings_modal_notice = ''; if ( 'billing_only' === $woocommerce_ship_to_destination ) { $settings_modal_notice = wp_kses( __( 'Woo Billing Address Module must be added to this page to allow users to submit orders.', 'et_builder' ), array( 'strong' => array() ) ); } else { $settings_modal_notice = wp_kses( __( 'Woo Billing Address Module and Woo Shipping Address Module must be added to this page to allow users to submit orders.', 'et_builder' ), array( 'strong' => array() ) ); } return $settings_modal_notice; } /** * Stop WooCommerce from redirecting Checkout page to Cart when the cart is empty. * * Divi Builder stops redirection only for logged-in admins. * * @since 4.14.0 */ function et_builder_wc_template_redirect() { $checkout_page_id = wc_get_page_id( 'checkout' ); $post = get_post( $checkout_page_id ); if ( ! ( $post instanceof WP_Post ) ) { return; } $is_checkout_page = $checkout_page_id === $post->ID; if ( ! $is_checkout_page ) { return; } if ( ! et_core_is_fb_enabled() ) { return; } if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) { return; } $has_wc_shortcode = has_shortcode( $post->post_content, 'et_pb_section' ); if ( ! $has_wc_shortcode ) { return; } add_filter( 'woocommerce_checkout_redirect_empty_cart', '__return_false' ); } /** * Sets the meta to indicate that the Divi content has been modified. * * This avoids setting the default WooCommerce Modules layout more than once. * * @link https://github.com/elegantthemes/Divi/issues/16420 * * @since 4.14.0 * * @param int $post_id Post ID. */ function et_builder_wc_set_page_content_status( $post_id ) { if ( 0 === absint( $post_id ) ) { return; } /** * The ID page of the Checkout page set in WooCommerce Settings page. * * WooCommerce — Settings — Advanced — Checkout page */ $checkout_page_id = wc_get_page_id( 'checkout' ); /** * The ID page of the Cart page set in WooCommerce Settings page. * * WooCommerce — Settings — Advanced — Cart page */ $cart_page_id = wc_get_page_id( 'cart' ); $is_cart = $post_id === $cart_page_id; $is_checkout = $post_id === $checkout_page_id; $is_product = 'product' === get_post_type( $post_id ); // Take action only on Product, Cart and Checkout pages. Bail early otherwise. if ( ! ( $is_product || $is_cart || $is_checkout ) ) { return; } $modified_status = 'modified'; $is_content_status_modified = get_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_PAGE_CONTENT_STATUS_META_KEY, true ) === $modified_status; if ( $is_content_status_modified ) { return; } update_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_PAGE_CONTENT_STATUS_META_KEY, $modified_status ); } /** * Gets the prefilled Cart Page content built using Divi Woo Modules. * * @since 4.14.0 * * @return string */ function et_builder_wc_get_prefilled_cart_page_content() { $page_title = '[et_pb_post_title meta="off" featured_image="off"][/et_pb_post_title]'; // Gets Parent theme's info in case child theme is used. if ( 'Extra' === et_core_get_theme_info( 'Name' ) ) { $page_title = ''; } return ' [et_pb_section] [et_pb_row] [et_pb_column type="4_4"] ' . $page_title . ' [et_pb_wc_cart_notice page_type="cart"][/et_pb_wc_cart_notice] [et_pb_wc_cart_products][/et_pb_wc_cart_products] [/et_pb_column] [/et_pb_row] [et_pb_row column_structure="1_2,1_2"] [et_pb_column type="1_2"] [et_pb_wc_cross_sells][/et_pb_wc_cross_sells] [/et_pb_column] [et_pb_column type="1_2"] [et_pb_wc_cart_totals][/et_pb_wc_cart_totals] [/et_pb_column] [/et_pb_row] [/et_pb_section] '; } /** * Gets the prefilled Checkout Page content built using Divi Woo Modules. * * @since 4.14.0 * @return string */ function et_builder_wc_get_prefilled_checkout_page_content() { $page_title = '[et_pb_post_title meta="off" featured_image="off"][/et_pb_post_title]'; // Use `et_core_get_theme_info` to get Parent theme's info even when a child theme is used. if ( 'Extra' === et_core_get_theme_info( 'Name' ) ) { $page_title = ''; } return ' [et_pb_section] [et_pb_row custom_padding="||0%||false|false"] [et_pb_column type="4_4"] ' . $page_title . ' [et_pb_wc_cart_notice page_type="checkout"][/et_pb_wc_cart_notice] [/et_pb_column] [/et_pb_row] [et_pb_row column_structure="1_2,1_2"] [et_pb_column type="1_2"] [et_pb_wc_checkout_billing ][/et_pb_wc_checkout_billing] [/et_pb_column] [et_pb_column type="1_2"] [et_pb_wc_checkout_shipping][/et_pb_wc_checkout_shipping] [et_pb_wc_checkout_additional_info][/et_pb_wc_checkout_additional_info] [/et_pb_column] [/et_pb_row] [et_pb_row] [et_pb_column type="4_4"] [et_pb_wc_checkout_order_details][/et_pb_wc_checkout_order_details] [et_pb_wc_checkout_payment_info][/et_pb_wc_checkout_payment_info] [/et_pb_column] [/et_pb_row] [/et_pb_section] '; } /** * Sets the pre-filled Divi Woo Pages layout content. * * The following are the three types of WooCommerce pages that have pre-filled content. * * 1. WooCommerce Product page * 2. WooCommerce Cart page * 3. WooCommerce Checkout page * * @param string $maybe_shortcode_content May be shortcode content. * @param int $post_id Post ID. * * @return string */ function et_builder_wc_set_prefilled_page_content( $maybe_shortcode_content, $post_id ) { $post = get_post( absint( $post_id ) ); if ( ! $post ) { return $maybe_shortcode_content; } /** * The ID page of the Checkout page set in WooCommerce Settings page. * * WooCommerce — Settings — Advanced — Checkout page */ $checkout_page_id = wc_get_page_id( 'checkout' ); /** * The ID page of the Cart page set in WooCommerce Settings page. * * WooCommerce — Settings — Advanced — Cart page */ $cart_page_id = wc_get_page_id( 'cart' ); $is_cart = $post_id === $cart_page_id; $is_checkout = $post_id === $checkout_page_id; $is_product = ( $post instanceof WP_Post ) && 'product' === $post->post_type; // Bail early when none of the conditions are met. if ( ! ( $is_product || $is_checkout || $is_cart ) ) { return $maybe_shortcode_content; } // Bail early if the Page already has initial content set. $is_content_status_modified = 'modified' === get_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_PAGE_CONTENT_STATUS_META_KEY, true ); if ( $is_content_status_modified ) { return $maybe_shortcode_content; } $should_replace_content = true; if ( $is_cart || $is_checkout ) { $should_replace_content = et_builder_wc_should_replace_content( $maybe_shortcode_content ); } if ( $is_cart && $should_replace_content ) { return et_builder_wc_get_prefilled_cart_page_content(); } elseif ( $is_checkout && $should_replace_content ) { return et_builder_wc_get_prefilled_checkout_page_content(); } elseif ( $is_product ) { $args = array(); $product_page_layout = et_builder_wc_get_product_layout( $post_id ); /* * When FALSE, this means the Product doesn't use Builder at all; * Or the Product has been using the Builder before WooCommerce Modules QF launched. */ if ( ! $product_page_layout ) { $product_page_layout = et_get_option( 'et_pb_woocommerce_page_layout', 'et_build_from_scratch' ); } // Load default content. if ( 'et_default_layout' === $product_page_layout ) { return $maybe_shortcode_content; } $has_et_builder_shortcode = has_shortcode( $maybe_shortcode_content, 'et_pb_section' ); $is_layout_type_build_from_scratch = 'et_build_from_scratch' === $product_page_layout; if ( $has_et_builder_shortcode && $is_layout_type_build_from_scratch ) { $args['existing_shortcode'] = $maybe_shortcode_content; } return et_builder_wc_get_prefilled_product_page_content( $args ); } return $maybe_shortcode_content; } /** * Returning string for default image placeholder * * @since 4.14.0 Added $mode param. * @since 4.0.10 * * @param string $mode Default ET_BUILDER_PLACEHOLDER_LANDSCAPE_IMAGE_DATA. Either Landscape or * Portrait image mode. * * @return string */ function et_builder_wc_placeholder_img( $mode = 'portrait' ) { $allowed_list = array( 'portrait' => ET_BUILDER_PLACEHOLDER_PORTRAIT_VARIATION_IMAGE_DATA, 'landscape' => ET_BUILDER_PLACEHOLDER_LANDSCAPE_IMAGE_DATA, ); if ( ! in_array( $mode, array_keys( $allowed_list ), true ) ) { $mode = 'portrait'; } return sprintf( '', et_core_esc_attr( 'placeholder', $allowed_list[ $mode ] ), esc_attr__( 'Product image', 'et_builder' ) ); } /** * Gets the Product Content options. * * This array is used in Divi Page Settings metabox and in Divi Theme Options ⟶ Builder ⟶ Post Type integration. * * @since 3.29 * * @param string $translation_context Translation Context to indicate if translation origins from Divi Theme or * from the Builder. Optional. Default 'et_builder'. * * @return array */ function et_builder_wc_get_page_layouts( $translation_context = 'et_builder' ) { switch ( $translation_context ) { case 'Divi': $product_page_layouts = array( 'et_build_from_scratch' => esc_html__( 'Build From Scratch', 'Divi' ), 'et_default_layout' => esc_html__( 'Default', 'Divi' ), ); break; default: $product_page_layouts = array( 'et_build_from_scratch' => esc_html__( 'Build From Scratch', 'et_builder' ), 'et_default_layout' => et_builder_i18n( 'Default' ), ); break; } return $product_page_layouts; } /** * Adds WooCommerce Module settings to the Builder settings. * * Adding in the Builder Settings tab will ensure that the field is available in Extra Theme and * Divi Builder Plugin. * * @since 4.0.3 Hide Product Content layout settings Divi Builder Plugin options. * @since 3.29 * * @param array $builder_settings_fields Builder settings fields. * * @return array */ function et_builder_wc_add_settings( $builder_settings_fields ) { // Bail early to hide WooCommerce Settings tab under the Builder tab. // If $fields['tab_slug'] is not equal to the tab slug (i.e. woocommerce_page_layout) then WooCommerce settings tab won't be displayed. // {@see ET_Builder_Settings::_get_builder_settings_in_epanel_format}. if ( ! et_is_woocommerce_plugin_active() ) { return $builder_settings_fields; } $fields = array( 'et_pb_woocommerce_product_layout' => array( 'type' => 'select', 'id' => 'et_pb_woocommerce_product_layout', 'index' => - 1, 'label' => esc_html__( 'Product Layout', 'et_builder' ), 'description' => esc_html__( 'Here you can choose Product Page Layout for WooCommerce.', 'et_builder' ), 'options' => array( 'et_right_sidebar' => esc_html__( 'Right Sidebar', 'et_builder' ), 'et_left_sidebar' => esc_html__( 'Left Sidebar', 'et_builder' ), 'et_no_sidebar' => esc_html__( 'No Sidebar', 'et_builder' ), 'et_full_width_page' => esc_html__( 'Fullwidth', 'et_builder' ), ), 'default' => 'et_right_sidebar', 'validation_type' => 'simple_text', 'et_save_values' => true, 'tab_slug' => 'post_type_integration', 'toggle_slug' => 'performance', ), 'et_pb_woocommerce_page_layout' => array( 'type' => 'select', 'id' => 'et_pb_woocommerce_product_page_layout', 'index' => -1, 'label' => esc_html__( 'Product Content', 'et_builder' ), 'description' => esc_html__( '"Build From Scratch" loads a pre-built WooCommerce page layout, with which you build on when the Divi Builder is enabled. "Default" option lets you use default WooCommerce page layout.', 'et_builder' ), 'options' => et_builder_wc_get_page_layouts(), 'default' => 'et_build_from_scratch', 'validation_type' => 'simple_text', 'et_save_values' => true, 'tab_slug' => 'post_type_integration', 'toggle_slug' => 'performance', ), ); // Hide setting in DBP : https://github.com/elegantthemes/Divi/issues/17378. if ( et_is_builder_plugin_active() ) { unset( $fields['et_pb_woocommerce_product_layout'] ); } return array_merge( $builder_settings_fields, $fields ); } /** * Gets the pre-built layout for WooCommerce product pages. * * @since 3.29 * * @param array $args { * Additional args. * * @type string $existing_shortcode Existing builder shortcode. * } * * @return string */ function et_builder_wc_get_prefilled_product_page_content( $args = array() ) { /** * Filters the Top section Background in the default WooCommerce Modules layout. * * @param string $color Default empty. */ $et_builder_wc_initial_top_section_bg = apply_filters( 'et_builder_wc_initial_top_section_bg', '' ); $content = ' [et_pb_section custom_padding="0px||||false|false" background_color="' . esc_attr( $et_builder_wc_initial_top_section_bg ) . '"] [et_pb_row width="100%" custom_padding="0px||0px||false|false"] [et_pb_column type="4_4"] [et_pb_wc_breadcrumb][/et_pb_wc_breadcrumb] [et_pb_wc_cart_notice][/et_pb_wc_cart_notice] [/et_pb_column] [/et_pb_row] [et_pb_row custom_padding="0px||||false|false" width="100%"] [et_pb_column type="1_2"] [et_pb_wc_images][/et_pb_wc_images] [/et_pb_column] [et_pb_column type="1_2"] [et_pb_wc_title][/et_pb_wc_title] [et_pb_wc_rating][/et_pb_wc_rating] [et_pb_wc_price][/et_pb_wc_price] [et_pb_wc_description][/et_pb_wc_description] [et_pb_wc_add_to_cart form_field_text_align="center"][/et_pb_wc_add_to_cart] [et_pb_wc_meta][/et_pb_wc_meta] [/et_pb_column] [/et_pb_row] [et_pb_row width="100%"] [et_pb_column type="4_4"] [et_pb_wc_tabs] [/et_pb_wc_tabs] [et_pb_wc_upsells columns_number="3"][/et_pb_wc_upsells] [et_pb_wc_related_products columns_number="3"][/et_pb_wc_related_products] [/et_pb_column] [/et_pb_row] [/et_pb_section]'; if ( ! empty( $args['existing_shortcode'] ) ) { return $content . $args['existing_shortcode']; } return $content; } /** * Gets the Product layout for a given Post ID. * * @since 3.29 * * @param int $post_id Post Id. * * @return string The return value will be one of the values from * {@see et_builder_wc_get_page_layouts()} when the Post ID is valid. * Empty string otherwise. */ function et_builder_wc_get_product_layout( $post_id ) { $post = get_post( $post_id ); if ( ! $post ) { return false; } return get_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_PAGE_LAYOUT_META_KEY, true ); } /** * Sets the pre-built layout for WooCommerce product pages. * * @param string $maybe_shortcode_content Post content. * @param int $post_id Post id. * * @return string */ function et_builder_wc_set_initial_content( $maybe_shortcode_content, $post_id ) { $post = get_post( absint( $post_id ) ); $args = array(); if ( ! ( $post instanceof WP_Post ) || 'product' !== $post->post_type ) { return $maybe_shortcode_content; } // $post_id is a valid Product ID by now. $product_page_layout = et_builder_wc_get_product_layout( $post_id ); /* * When FALSE, this means the Product doesn't use Builder at all; * Or the Product has been using the Builder before WooCommerce Modules QF launched. */ if ( ! $product_page_layout ) { $product_page_layout = et_get_option( 'et_pb_woocommerce_page_layout', 'et_build_from_scratch' ); } $is_product_content_modified = 'modified' === get_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_PAGE_CONTENT_STATUS_META_KEY, true ); // Content was already saved or default content should be loaded. if ( $is_product_content_modified || 'et_default_layout' === $product_page_layout ) { return $maybe_shortcode_content; } if ( has_shortcode( $maybe_shortcode_content, 'et_pb_section' ) && 'et_build_from_scratch' === $product_page_layout && ! empty( $maybe_shortcode_content ) ) { $args['existing_shortcode'] = $maybe_shortcode_content; } return et_builder_wc_get_prefilled_product_page_content( $args ); } /** * Saves the WooCommerce long description metabox content. * * The content is stored as post meta w/ the key `_et_pb_old_content`. * * @param int $post_id Post id. * @param WP_Post $post Post Object. * @param array $request The $_POST Request variables. * * @since 3.29 */ function et_builder_wc_long_description_metabox_save( $post_id, $post, $request ) { if ( ! isset( $request['et_bfb_long_description_nonce'] ) ) { return; } if ( current_user_can( 'edit_posts', $post_id ) && et_core_security_check( 'edit_posts', 'et_bfb_long_description_nonce', '_et_bfb_long_description_nonce', '_POST', false ) ) { return; } if ( 'product' !== $post->post_type ) { return; } if ( ! isset( $request['et_builder_wc_product_long_description'] ) ) { return; } $long_desc_content = $request['et_builder_wc_product_long_description']; $is_updated = update_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_LONG_DESC_META_KEY, wp_kses_post( $long_desc_content ) ); } /** * Output Callback for Product long description metabox. * * @since 3.29 * * @param WP_Post $post Post. */ function et_builder_wc_long_description_metabox_render( $post ) { $settings = array( 'textarea_name' => 'et_builder_wc_product_long_description', 'quicktags' => array( 'buttons' => 'em,strong,link' ), 'tinymce' => array( 'theme_advanced_buttons1' => 'bold,italic,strikethrough,separator,bullist,numlist,separator,blockquote,separator,justifyleft,justifycenter,justifyright,separator,link,unlink,separator,undo,redo,separator', 'theme_advanced_buttons2' => '', ), 'editor_css' => '', ); // Since we use $post_id in more than one place, use a variable. $post_id = $post->ID; // Long description metabox content. Default Empty. $long_desc_content = get_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_LONG_DESC_META_KEY, true ); $long_desc_content = ! empty( $long_desc_content ) ? $long_desc_content : ''; /** * Filters the wp_editor settings used in the Long description metabox. * * @param array $settings WP Editor settings. * * @since 3.29 */ $settings = apply_filters( 'et_builder_wc_product_long_description_editor_settings', $settings ); wp_nonce_field( '_et_bfb_long_description_nonce', 'et_bfb_long_description_nonce' ); wp_editor( $long_desc_content, 'et_builder_wc_product_long_description', $settings ); } /** * Adds the Long description metabox to Product post type. * * @since 3.29 * * @param WP_Post $post WP Post. */ function et_builder_wc_long_description_metabox_register( $post ) { if ( 'on' !== get_post_meta( $post->ID, '_et_pb_use_builder', true ) ) { return; } add_meta_box( 'et_builder_wc_product_long_description_metabox', __( 'Product long description', 'et_builder' ), 'et_builder_wc_long_description_metabox_render', 'product', 'normal' ); } /** * Determine if WooCommerce's $product global need to be overwritten or not. * IMPORTANT: make sure to reset it later * * @since 3.29 * * @param string $product_id Post id. * * @return bool */ function et_builder_wc_need_overwrite_global( $product_id = 'current' ) { $is_current_product_page = 'current' === $product_id; // There are three situation which requires global value overwrite: initial builder // ajax request, computed callback jax request (all ajax request has faulty global variable), // and if `product` attribute is not current page's product id (ie Woo Tabs being used // on non `product` CPT). $need_overwrite_global = ! $is_current_product_page || et_fb_is_builder_ajax() || et_fb_is_computed_callback_ajax(); return $need_overwrite_global; } /** * Gets the Product ID. * * @since 4.14.0 * * @param array $args Module props. * * @return int $product_id */ function et_builder_wc_get_product_id( $args ) { $maybe_product_id = et_()->array_get( $args, 'product', 'latest' ); $is_latest_product = 'latest' === $maybe_product_id; $is_current_product_page = 'current' === $maybe_product_id; if ( $is_latest_product ) { // Dynamic filter's product_id need to be translated into correct id. $product_id = ET_Builder_Module_Helper_Woocommerce_Modules::get_product_id( $maybe_product_id ); } elseif ( $is_current_product_page && wp_doing_ajax() && class_exists( 'ET_Builder_Element' ) ) { /* * $product global doesn't exist in ajax request; thus get the fallback post id * this is likely happen in computed callback ajax request. */ $product_id = ET_Builder_Element::get_current_post_id(); } else { // Besides two situation above, $product_id is current $args['product']. if ( false !== get_post_status( $maybe_product_id ) ) { $product_id = $maybe_product_id; } else { // Fallback to Latest product if saved product ID doesn't exist. $product_id = ET_Builder_Module_Helper_Woocommerce_Modules::get_product_id( 'latest' ); } } return $product_id; } /** * Helper to render module template for module's front end and computed callback output * * @since 3.29 * * @param string $function_name Rendering method name. * @param array $args Method arguments. * @param array $overwrite List of global variables to overwrites e.g $product, $post and $wp_query. * * @return string */ function et_builder_wc_render_module_template( $function_name, $args = array(), $overwrite = array( 'product' ) ) { // Shouldn't be fired in Backend to not break the BB loading. if ( is_admin() && ! wp_doing_ajax() ) { return; } // Check if passed function name is allowlisted or not. $allowlisted_functions = array( 'the_title', 'woocommerce_breadcrumb', 'woocommerce_template_single_price', 'woocommerce_template_single_add_to_cart', 'woocommerce_product_additional_information_tab', 'woocommerce_template_single_meta', 'woocommerce_template_single_rating', 'woocommerce_show_product_images', 'wc_get_stock_html', 'wc_print_notices', 'wc_print_notice', 'woocommerce_output_related_products', 'woocommerce_upsell_display', 'woocommerce_checkout_login_form', 'wc_cart_empty_template', 'woocommerce_output_all_notices', ); if ( ! in_array( $function_name, $allowlisted_functions, true ) ) { return ''; } // phpcs:disable WordPress.WP.GlobalVariablesOverride -- Overwrite global variables when rendering templates which are restored before this function exist. global $product, $post, $wp_query; $defaults = array( 'product' => 'current', ); $args = wp_parse_args( $args, $defaults ); $overwrite_global = et_builder_wc_need_overwrite_global( $args['product'] ); $overwrite_product = in_array( 'product', $overwrite, true ); $overwrite_post = in_array( 'post', $overwrite, true ); $overwrite_wp_query = in_array( 'wp_query', $overwrite, true ); $is_tb = et_builder_tb_enabled(); $is_use_placeholder = $is_tb || is_et_pb_preview(); if ( $is_use_placeholder ) { // global object needs to be set before output rendering. This needs to be performed on each // module template rendering instead of once for all module template rendering because some // module's template rendering uses `wp_reset_postdata()` which resets global query. et_theme_builder_wc_set_global_objects(); } elseif ( $overwrite_global ) { $product_id = et_builder_wc_get_product_id( $args ); if ( 'product' !== get_post_type( $product_id ) ) { // We are in a Theme Builder layout and the current post is not a product - use the latest one instead. $products = new WP_Query( array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => 1, 'no_found_rows' => true, ) ); if ( ! $products->have_posts() ) { return ''; } $product_id = $products->posts[0]->ID; } // Overwrite product. if ( $overwrite_product ) { $original_product = $product; $product = wc_get_product( $product_id ); } // Overwrite post. if ( $overwrite_post ) { $original_post = $post; $post = get_post( $product_id ); } // Overwrite wp_query. if ( $overwrite_wp_query ) { $original_wp_query = $wp_query; $wp_query = new WP_Query( array( 'p' => $product_id ) ); } } ob_start(); switch ( $function_name ) { case 'woocommerce_breadcrumb': $breadcrumb_separator = et_()->array_get( $args, 'breadcrumb_separator', '' ); $breadcrumb_separator = str_replace( '”', '', $breadcrumb_separator ); woocommerce_breadcrumb( array( 'delimiter' => ' ' . $breadcrumb_separator . ' ', 'home' => et_()->array_get( $args, 'breadcrumb_home_text', '' ), ) ); break; case 'woocommerce_show_product_images': if ( is_a( $product, 'WC_Product' ) ) { // WC Images module needs to modify global variable's property. Thus it is performed // here instead at module's class since the $product global might be modified. $gallery_ids = $product->get_gallery_image_ids(); $image_id = $product->get_image_id(); $show_image = 'on' === $args['show_product_image']; $show_gallery = 'on' === $args['show_product_gallery']; $show_sale_badge = 'on' === $args['show_sale_badge']; // If featured image is disabled, replace it with first gallery image's id (if gallery // is enabled) or replaced it with empty string (if gallery is disabled as well). if ( ! $show_image ) { if ( $show_gallery && isset( $gallery_ids[0] ) ) { $product->set_image_id( $gallery_ids[0] ); // Remove first image from the gallery because it'll be added as thumbnail and will be duplicated. unset( $gallery_ids[0] ); $product->set_gallery_image_ids( $gallery_ids ); } else { $product->set_image_id( '' ); } } // Replaced gallery image ids with empty array. if ( ! $show_gallery ) { $product->set_gallery_image_ids( array() ); } if ( $show_sale_badge && function_exists( 'woocommerce_show_product_sale_flash' ) ) { woocommerce_show_product_sale_flash(); } // @phpcs:ignore Generic.PHP.ForbiddenFunctions.Found -- Using for consistency. call_user_func( $function_name ); // Reset product's actual featured image id. if ( ! $show_image ) { $product->set_image_id( $image_id ); } // Reset product's actual gallery image id. if ( ! $show_gallery ) { $product->set_gallery_image_ids( $gallery_ids ); } } break; case 'wc_get_stock_html': if ( is_a( $product, 'WC_Product' ) ) { echo wc_get_stock_html( $product ); // phpcs:ignore WordPress.Security.EscapeOutput -- `wc_get_stock_html` include woocommerce's `single-product/stock.php` template. } break; case 'wc_print_notice': $message = et_()->array_get( $args, 'wc_cart_message', '' ); // @phpcs:ignore Generic.PHP.ForbiddenFunctions.Found call_user_func( $function_name, $message ); break; case 'wc_print_notices': if ( isset( WC()->session ) ) { // Save existing notices to restore them as many times as we need. $et_wc_cached_notices = WC()->session->get( 'wc_notices', array() ); // @phpcs:ignore Generic.PHP.ForbiddenFunctions.Found -- Using for consistency. call_user_func( $function_name ); // Restore notices which were removed after wc_print_notices() executed to render multiple modules on page. if ( ! empty( $et_wc_cached_notices ) && empty( WC()->session->get( 'wc_notices', array() ) ) ) { WC()->session->set( 'wc_notices', $et_wc_cached_notices ); } } break; case 'woocommerce_checkout_login_form': if ( function_exists( 'woocommerce_checkout_login_form' ) ) { woocommerce_checkout_login_form(); } if ( function_exists( 'woocommerce_checkout_coupon_form' ) ) { woocommerce_checkout_coupon_form(); } $is_builder = et_()->array_get( $args, 'is_builder', false ); if ( $is_builder ) { ET_Builder_Module_Woocommerce_Cart_Notice::output_coupon_error_message(); } break; case 'woocommerce_upsell_display': $order = isset( $args['order'] ) ? $args['order'] : ''; // @phpcs:ignore Generic.PHP.ForbiddenFunctions.Found call_user_func( $function_name, '', '', '', $order ); break; case 'wc_cart_empty_template': wc_get_template( 'cart/cart-empty.php' ); break; case 'woocommerce_output_all_notices': if ( isset( WC()->session ) ) { // Save existing notices to restore them as many times as we need. $et_wc_cached_notices = WC()->session->get( 'wc_notices', array() ); if ( function_exists( $function_name ) ) { // @phpcs:ignore Generic.PHP.ForbiddenFunctions.Found -- Using for consistency. call_user_func( $function_name ); } // Restore notices which were removed after wc_print_notices() executed to render multiple modules on page. if ( ! empty( $et_wc_cached_notices ) && empty( WC()->session->get( 'wc_notices', array() ) ) ) { WC()->session->set( 'wc_notices', $et_wc_cached_notices ); } } break; case 'woocommerce_template_single_price': case 'woocommerce_template_single_meta': if ( is_a( $product, 'WC_Product' ) ) { /* * Variable functions. * @see https://www.php.net/manual/en/functions.variable-functions.php */ $function_name(); } break; default: // Only whitelisted functions shall be allowed until this point of execution. if ( is_a( $product, 'WC_Product' ) ) { // @phpcs:ignore Generic.PHP.ForbiddenFunctions.Found -- Only whitelisted functions reach here. call_user_func( $function_name ); } } $output = ob_get_clean(); // Reset original product variable to global $product. if ( $is_use_placeholder ) { et_theme_builder_wc_reset_global_objects(); } elseif ( $overwrite_global ) { // Reset $product global. if ( $overwrite_product ) { $product = $original_product; } // Reset post. if ( $overwrite_post ) { $post = $original_post; } // Reset wp_query. if ( $overwrite_wp_query ) { $wp_query = $original_wp_query; } // phpcs:enable WordPress.WP.GlobalVariablesOverride -- Enable global variable override check. } return $output; } /** * Renders the content. * * Rendering the content will enable Divi Builder to take over the entire * post content area. * * @since 3.29 */ function et_builder_wc_product_render_layout() { do_action( 'et_builder_wc_product_before_render_layout' ); the_content(); do_action( 'et_builder_wc_product_after_render_layout' ); } /** * Force WooCommerce to load default template over theme's custom template when builder's * et_builder_from_scratch is used to prevent unexpected custom layout which makes builder * experience inconsistent * * @since 3.29 * * @param string $template Path to template file. * @param string $slug Template slug. * @param string $name Template name. * * @return string */ function et_builder_wc_override_template_part( $template, $slug, $name ) { // Only force load default `content-single-product.php` template. $is_content_single_product = 'content' === $slug && 'single-product' === $name; return $is_content_single_product ? WC()->plugin_path() . "/templates/{$slug}-{$name}.php" : $template; } /** * Disable all default WooCommerce single layout hooks. * * @since 4.0.10 */ function et_builder_wc_disable_default_layout() { // To remove a hook, the $function_to_remove and $priority arguments must match // with which the hook was added. remove_action( 'woocommerce_before_main_content', 'woocommerce_breadcrumb', 20 ); remove_action( 'woocommerce_before_single_product_summary', 'woocommerce_show_product_sale_flash', 10 ); remove_action( 'woocommerce_before_single_product_summary', 'woocommerce_show_product_images', 20 ); remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_title', 5 ); remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_rating', 10 ); remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_price', 10 ); remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_excerpt', 20 ); remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_add_to_cart', 30 ); remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_meta', 40 ); remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_sharing', 50 ); remove_action( 'woocommerce_after_single_product_summary', 'woocommerce_output_product_data_tabs', 10 ); remove_action( 'woocommerce_after_single_product_summary', 'woocommerce_upsell_display', 15 ); remove_action( 'woocommerce_after_single_product_summary', 'woocommerce_output_related_products', 20 ); } /** * Relocate all registered callbacks from `woocommerce_single_product_summary` hook to * any suitable Woo modules. * * @since 4.14.5 * @since 4.15.0 Move relocation process into outside callbacks loop to avoid duplication. */ function et_builder_wc_relocate_single_product_summary() { global $post, $wp_filter; if ( ! $post ) { return; } $tb_body_layout = ET_THEME_BUILDER_BODY_LAYOUT_POST_TYPE; $tb_body_override = et_theme_builder_overrides_layout( $tb_body_layout ); $tb_layouts = et_theme_builder_get_template_layouts(); $tb_body_layout_id = $tb_body_override ? $tb_layouts[$tb_body_layout]['id'] : false; $tb_body_content = $tb_body_layout_id ? get_post_field( 'post_content', $tb_body_layout_id ) : ''; $has_wc_module = et_builder_has_woocommerce_module( $post->post_content ); $has_wc_module_tb = et_builder_has_woocommerce_module( $tb_body_content ); $hook = et_()->array_get( $wp_filter, 'woocommerce_single_product_summary', null ); // Bail early if there is no `woocommerce_single_product_summary` hook callbacks or // if there is no WooCommerce module in the content of current page and TB body layout. if ( ( ! $has_wc_module && ! $has_wc_module_tb ) || empty( $hook->callbacks ) ) { return; } $is_copy_needed = false; $is_move_needed = false; $post_id = ! empty( $post->ID ) ? $post->ID : false; // Product related pages. $is_product = function_exists( 'is_product' ) && is_product(); $is_shop = function_exists( 'is_shop' ) && is_shop(); $is_product_category = function_exists( 'is_product_category' ) && is_product_category(); $is_product_tag = function_exists( 'is_product_tag' ) && is_product_tag(); // Copy single product summary hooks when current page is: // - Product related pages: single, shop, category, & tag. // - Theme Builder or Page Builder. // - Before & after components AJAX request. // - Has TB layouts contain WC modules. if ( $is_product || $is_shop || $is_product_category || $is_product_tag || et_builder_tb_enabled() || et_core_is_fb_enabled() || et_fb_is_before_after_components_callback_ajax() || et_builder_wc_is_non_product_post_type() ) { $is_copy_needed = true; } // Move single product summary hooks when current page is single product with: // - Builder is used. // - TB Body layout overrides the content. if ( $is_product ) { if ( et_pb_is_pagebuilder_used( $post_id ) || $tb_body_override ) { $is_move_needed = true; } } /** * Filters whether to copy single product summary hooks output or not. * * 3rd-party plugins can use this filter to force enable or disable this action. * * @since 4.14.5 * * @param boolean $is_copy_needed Whether to copy single product summary or not. */ $is_copy_needed = apply_filters( 'et_builder_wc_relocate_single_product_summary_is_copy_needed', $is_copy_needed ); /** * Filters whether to move (remove the original) single product summary or not. * * 3rd-party plugins can use this filter to force enable or disable this action. * * @since 4.14.5 * * @param boolean $is_move_needed Whether to move single product summary or not. */ $is_move_needed = apply_filters( 'et_builder_wc_relocate_single_product_summary_is_move_needed', $is_move_needed ); // Bail early if copy action is not needed. if ( ! $is_copy_needed ) { return; } $modules_with_relocation = array(); /** * Filters the list of ignored `woocommerce_single_product_summary` hook callbacks. * * 3rd-party plugins can use this filter to keep their callbacks so they won't be * relocated from `woocommerce_single_product_summary` hook. The value is string of * `function_name` or `class::method` combination. By default, it contanis all single * product summary actions from WooCommerce plugin. * * @since 4.14.5 * * @param array $ignored_callbacks List of ignored callbacks. */ $ignored_callbacks = apply_filters( 'et_builder_wc_relocate_single_product_summary_ignored_callbacks', array( 'WC_Structured_Data::generate_product_data', 'woocommerce_template_single_title', 'woocommerce_template_single_rating', 'woocommerce_template_single_price', 'woocommerce_template_single_excerpt', 'woocommerce_template_single_add_to_cart', 'woocommerce_template_single_meta', 'woocommerce_template_single_sharing', ) ); // Pair of WooCommerce layout priority numbers and Woo module slugs. $modules_priority = array( '5' => 'et_pb_wc_title', '10' => 'et_pb_wc_price', // `et_pb_wc_rating` also has the same priority. '20' => 'et_pb_wc_description', // It's `excerpt` on WooCommerce default layout. '30' => 'et_pb_wc_add_to_cart', '40' => 'et_pb_wc_meta', ); foreach ( $hook->callbacks as $callback_priority => $callbacks ) { foreach ( $callbacks as $callback_args ) { // 1. Generate 'callback name' (string). // Get the callback name stored on the `function` argument. $callback_function = et_()->array_get( $callback_args, 'function' ); $callback_name = $callback_function; // Bail early if the callback is not callable to avoid any unexpected issue. if ( ! is_callable( $callback_function ) ) { continue; } // If the `function` is an array, it's probably a class based function. We should // convert it into string based callback name for validating purpose. if ( is_array( $callback_function ) ) { $callback_name = ''; $callback_object = et_()->array_get( $callback_function, 0 ); $callback_method = et_()->array_get( $callback_function, 1 ); // Ensure the index `0` is an object and the index `1` is string. We're going to // use the class::method combination as callback name. if ( is_object( $callback_object ) && is_string( $callback_method ) ) { $callback_class = get_class( $callback_object ); $callback_name = "{$callback_class}::{$callback_method}"; } } // Bail early if callback name is not string or empty to avoid unexpected issues. if ( ! is_string( $callback_name ) || empty( $callback_name ) ) { continue; } // Bail early if current callback is listed on ignored callbacks list. if ( in_array( $callback_name, $ignored_callbacks, true ) ) { continue; } // 2. Generate 'module priority' to get suitable 'module slug'. // Find the module priority number by round down the priority to the nearest 10. // It's needed to get suitable Woo module. For example, a callback with priority // 41 means we have to put it on module with priority 40 which is `et_pb_wc_meta`. $rounded_callback_priority = intval( floor( $callback_priority / 10 ) * 10 ); $module_priority = $rounded_callback_priority; // Additional rules for module priority: // - 0 : Make it 5 as default to target `et_pb_wc_title` because there is no // module with priority less than 5. // - 50 : Make it 40 as default to target `et_pb_wc_meta` because there is no // module with priority more than 40. if ( 0 === $rounded_callback_priority ) { $module_priority = 5; } elseif ( $rounded_callback_priority >= 50 ) { $module_priority = 40; } $module_slug = et_()->array_get( $modules_priority, $module_priority ); /** * Filters target module for the current callback. * * 3rd-party plugins can use this filter to target different module slug. * * @since 4.14.5 * * @param string $module_slug Module slug. * @param string $callback_name Callback name. * @param string $module_priority Module priority. */ $module_slug = apply_filters( 'et_builder_wc_relocate_single_product_summary_module_slug', $module_slug, $callback_name, $module_priority ); // Bail early if module slug is empty. if ( empty( $module_slug ) ) { continue; } // 3. Determine 'output location'. // Move the callback to the suitable Woo module. Since we can't call the action // inside the module render, we have to buffer the output and prepend/append it // to the module output or preview. By default, the default location is 'after' // the module output or preview. But, for priority less than 5, we have to put it // before the `et_pb_wc_title` because there is no module on that location. $output_location = $callback_priority < 5 ? 'before' : 'after'; /** * Filters output location for the current module and callback. * * 3rd-party plugins can use this filter to change the output location. * * @since 4.14.5 * * @param string $output_location Output location. * @param string $callback_name Callback name. * @param string $module_slug Module slug. * @param string $callback_priority Callback priority. */ $output_location = apply_filters( 'et_builder_wc_relocate_single_product_summary_output_location', $output_location, $callback_name, $module_slug, $callback_priority ); // Bail early if the output location is not 'before' or 'after'. if ( ! in_array( $output_location, array( 'before', 'after' ), true ) ) { continue; } // 4. Determine 'module output priority'. // Get the "{$module_slug}_{$hook_suffix_name}}" filter priority number by sum up // default hook priority number (10) and the remainder. This part is important, // so we can prepend and append the layout output more accurate. For example: // Callback A with priority 42 should be added after callback B with priority 41 // on `et_pb_wc_meta` module. So, "et_pb_wc_meta_{$hook_suffix_name}_output" hook // priority for callback A will be 12, meanwhile callback B will be 11. $remainder_priority = $rounded_callback_priority > 0 ? $callback_priority % 10 : $callback_priority - 5; $output_priority = 10 + $remainder_priority; /** * Filters module output priority number for the current module and callback. * * 3rd-party plugins can use this filter to rearrange the output priority. * * @since 4.14.5 * * @param string $output_priority Module output priority number. * @param string $callback_name Callback name. * @param string $module_slug Module slug. * @param string $callback_priority Callback priority. */ $output_priority = apply_filters( 'et_builder_wc_relocate_single_product_summary_output_priority', $output_priority, $callback_name, $module_slug, $callback_priority ); // Remove the callback from `woocommerce_single_product_summary` when it's needed. if ( $is_move_needed ) { remove_action( 'woocommerce_single_product_summary', $callback_function, $callback_priority ); } // And, copy and paste it to suitable location & module. add_action( "et_builder_wc_single_product_summary_{$output_location}_{$module_slug}", $callback_function, $output_priority ); $modules_with_relocation[] = $module_slug; } } // Finally, move it to suitable Woo modules. if ( ! empty( $modules_with_relocation ) ) { foreach ( $modules_with_relocation as $module_slug ) { // Builder - Before and/or after components. add_filter( "{$module_slug}_fb_before_after_components", 'et_builder_wc_single_product_summary_before_after_components', 10, 3 ); // FE - Shortcode output. add_filter( "{$module_slug}_shortcode_output", 'et_builder_wc_single_product_summary_module_output', 10, 3 ); } } } /** * Prepend and/or append callback output to the suitable module output on FE. * * @since 4.14.5 * * @param string $module_output Module output. * @param string $module_slug Module slug. * @param ET_Builder_Element $module_instance Module instance. * * @return string Processed module output. */ function et_builder_wc_single_product_summary_module_output( $module_output, $module_slug, $module_instance ) { // Bail early if module output is not string. if ( ! is_string( $module_output ) ) { return $module_output; } global $post, $product; $original_post = $post; $original_product = $product; $target_id = ''; $is_overwritten = false; if ( ! empty( $module_instance->props ) ) { // Get target ID if any. $target_id = et_()->array_get( $module_instance->props, 'product' ); $target_id = class_exists( 'ET_Builder_Element' ) ? ET_Builder_Module_Helper_Woocommerce_Modules::get_product_id( $target_id ) : $target_id; } // Determine whether global product and post objects need to be overwritten or not. if ( 'current' !== $target_id ) { $target_product = wc_get_product( $target_id ); if ( $target_product instanceof WC_Product ) { $is_overwritten = false; $product = $target_product; $post = get_post( $product->get_id() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride -- Overriding global post is safe as original $post has been restored at the end. } } // Get before & after outputs only if product is WC_Product instance. if ( $product instanceof WC_Product ) { $before_output = et_builder_wc_single_product_summary_before_module( $module_slug ); $after_output = et_builder_wc_single_product_summary_after_module( $module_slug ); $module_output = $before_output . $module_output . $after_output; } // Reset product and/or post object. if ( $is_overwritten ) { $product = $original_product; $post = $original_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride -- Restore global post. } return $module_output; } /** * Set callback output as before and/or after components on builder. * * @since 4.14.5 * * @param array $module_components Default module before & after components. * @param string $module_slug Module slug. * @param array $module_data Module data. * * @return array Processed module before & after components. */ function et_builder_wc_single_product_summary_before_after_components( $module_components, $module_slug, $module_data ) { // Bail early if module components variable is not an array. if ( ! is_array( $module_components ) ) { return $module_components; } global $post, $product; $original_post = $post; $original_product = $product; $target_id = ''; $overwritten_by = ''; $is_tb_enabled = et_builder_tb_enabled(); $is_fb_enabled = et_core_is_fb_enabled() || is_et_pb_preview(); if ( ! empty( $module_data ) ) { // Get target ID if any. $target_id = et_()->array_get( $module_data, array( 'module_attrs', 'product' ) ); $target_id = class_exists( 'ET_Builder_Element' ) ? ET_Builder_Module_Helper_Woocommerce_Modules::get_product_id( $target_id ) : $target_id; } // Determine whether global product and post objects need to be overwritten or not. // - Dummy product: TB and FB initial load. // - Target product: Components request from builder. if ( $is_tb_enabled || $is_fb_enabled ) { et_theme_builder_wc_set_global_objects( array( 'is_tb' => true ) ); $overwritten_by = 'dummy_product'; } elseif ( 'current' !== $target_id && et_fb_is_before_after_components_callback_ajax() ) { $target_product = wc_get_product( $target_id ); if ( $target_product instanceof WC_Product ) { $overwritten_by = 'target_product'; $product = $target_product; $post = get_post( $product->get_id() ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride -- Overriding global post is safe as original $post has been restored at the end. } } // Get before & after components only if product is WC_Product instance. if ( $product instanceof WC_Product ) { $default_before_component = et_()->array_get( $module_components, '__before_component', '' ); $default_after_component = et_()->array_get( $module_components, '__after_component', '' ); $current_before_component = et_builder_wc_single_product_summary_before_module( $module_slug ); $current_after_component = et_builder_wc_single_product_summary_after_module( $module_slug ); $module_components['has_components'] = true; $module_components['__before_component'] = $default_before_component . $current_before_component; $module_components['__after_component'] = $default_after_component . $current_after_component; } // Reset product and/or post object. if ( 'dummy_product' === $overwritten_by ) { et_theme_builder_wc_reset_global_objects( array( 'is_tb' => true ) ); } elseif ( 'target_product' === $overwritten_by ) { $product = $original_product; $post = $original_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride -- Restore global post. } return $module_components; } /** * Render single product summary before Woo module output. * * @since 4.14.5 * * @param string $module_slug Module slug. * * @return string Rendered output. */ function et_builder_wc_single_product_summary_before_module( $module_slug ) { ob_start(); /** * Fires additional output for single product summary before module output. * * @since 4.14.5 */ do_action( "et_builder_wc_single_product_summary_before_{$module_slug}" ); return ob_get_clean(); } /** * Render single product summary after Woo module output. * * @since 4.14.5 * * @param string $module_slug Module slug. * * @return string Rendered output. */ function et_builder_wc_single_product_summary_after_module( $module_slug ) { ob_start(); /** * Fires additional output for single product summary after module output. * * @since 4.14.5 */ do_action( "et_builder_wc_single_product_summary_after_{$module_slug}" ); return ob_get_clean(); } /** * Overrides the default WooCommerce layout. * * @see woocommerce/includes/wc-template-functions.php * * @since 3.29 */ function et_builder_wc_override_default_layout() { if ( ! is_singular( 'product' ) ) { return; } // global $post won't be available with `after_setup_theme` hook and hence `wp` hook is used. global $post; if ( ! et_pb_is_pagebuilder_used( $post->ID ) ) { return; } $product_page_layout = et_builder_wc_get_product_layout( $post->ID ); $is_product_content_modified = 'modified' === get_post_meta( $post->ID, ET_BUILDER_WC_PRODUCT_PAGE_CONTENT_STATUS_META_KEY, true ); $is_preview_loading = is_preview(); // BFB was enabled but page content wasn't saved yet. Load default layout on FE. if ( 'et_build_from_scratch' === $product_page_layout && ! $is_product_content_modified && ! $is_preview_loading ) { return; } /* * The `has_shortcode()` check does not work here. Hence solving the need using `strpos()`. * * The WHY behind the check is explained in the following issue. * @see https://github.com/elegantthemes/Divi/issues/16155 */ if ( ! $product_page_layout && ! et_core_is_fb_enabled() || ( $product_page_layout && 'et_build_from_scratch' !== $product_page_layout ) ) { return; } // Force use WooCommerce's default template if current theme is not Divi or Extra (handling // possible custom template on DBP / Child Theme). if ( ! in_array( wp_get_theme()->get( 'Name' ), array( 'Divi', 'Extra' ), true ) ) { add_filter( 'wc_get_template_part', 'et_builder_wc_override_template_part', 10, 3 ); } et_builder_wc_disable_default_layout(); do_action( 'et_builder_wc_product_before_render_layout_registration' ); // Add render content on product page. add_action( 'woocommerce_after_single_product_summary', 'et_builder_wc_product_render_layout', 5 ); } /** * Skips setting default content on Product post type during Builder activation. * * Otherwise, the description would be shown in both Product Tabs and at the end of the * default WooCommerce layout set at * * @see et_builder_wc_get_prefilled_product_page_content() * * @since 3.29 * * @param bool $flag Whether to skips the content activation. * @param WP_Post $post Post. * * @return bool */ function et_builder_wc_skip_initial_content( $flag, $post ) { if ( ! ( $post instanceof WP_Post ) ) { return $flag; } if ( 'product' !== $post->post_type ) { return $flag; } return true; } /** * Determine whether given content has WooCommerce module inside it or not * * @since 4.0 Added ET_Builder_Element class exists check. * @since 3.29 * * @param string $content Content. * * @return bool */ function et_builder_has_woocommerce_module( $content = '' ) { if ( ! class_exists( 'ET_Builder_Element' ) ) { return false; } $has_woocommerce_module = false; $woocommerce_modules = ET_Builder_Element::get_woocommerce_modules(); foreach ( $woocommerce_modules as $module ) { if ( has_shortcode( $content, $module ) ) { $has_woocommerce_module = true; // Stop the loop once any shortcode is found. break; } } return apply_filters( 'et_builder_has_woocommerce_module', $has_woocommerce_module ); } /** * Check if current global $post uses builder / layout block, not `product` CPT, and contains * WooCommerce module inside it. This check is needed because WooCommerce by default only adds * scripts and style to `product` CPT while WooCommerce Modules can be used at any CPT * * @since 3.29 * @since 4.1.0 check if layout block is used instead of builder * * @since bool */ function et_builder_wc_is_non_product_post_type() { if ( wp_doing_ajax() ) { return false; } global $post; if ( $post && 'product' === $post->post_type ) { return false; } $types = et_theme_builder_get_layout_post_types(); $layouts = et_theme_builder_get_template_layouts(); foreach ( $types as $type ) { if ( ! isset( $layouts[ $type ] ) ) { continue; } if ( $layouts[ $type ]['override'] && et_builder_has_woocommerce_module( get_post_field( 'post_content', $layouts[ $type ]['id'] ) ) ) { return true; } } // If no post found, bail early. if ( ! $post ) { return false; } $is_builder_used = et_pb_is_pagebuilder_used( $post->ID ); $is_layout_block_used = has_block( 'divi/layout', $post->post_content ); // If no builder or layout block used, bail early. if ( ! $is_builder_used && ! $is_layout_block_used ) { return false; } $has_wc_module = et_builder_has_woocommerce_module( $post->post_content ); if ( ( $is_builder_used || $is_layout_block_used ) && $has_wc_module ) { return true; } return false; } /** * Load WooCommerce related scripts. This function basically redo what * `WC_Frontend_Scripts::load_scripts()` does without the `product` CPT limitation. * * Once more WooCommerce Modules are added (checkout, account, etc), revisit this method and * compare it against `WC_Frontend_Scripts::load_scripts()`. Some of the script queues are * removed here because there is currently no WooCommerce module equivalent of them. * * @since 3.29 * @since 4.3.3 Loads WC scripts on Shop, Product Category & Product Tags archives. * @since 4.9.11 Avoid invalid argument supplied for foreach() warning. */ function et_builder_wc_load_scripts() { global $post; $is_shop = function_exists( 'is_shop' ) && is_shop(); $is_checkout = function_exists( 'is_checkout' ) && is_checkout(); // is_product_taxonomy() is not returning TRUE for Category & Tags. // Hence we check Category & Tag archives individually. $is_product_category = function_exists( 'is_product_category' ) && is_product_category(); $is_product_tag = function_exists( 'is_product_tag' ) && is_product_tag(); // If current page is not non-`product` CPT which using builder, stop early. if ( ( ! et_builder_wc_is_non_product_post_type() || ! class_exists( 'WC_Frontend_Scripts' ) ) && function_exists( 'et_fb_enabled' ) && ! et_core_is_fb_enabled() && ! $is_shop && ! $is_product_category && ! $is_product_tag && ! $is_checkout ) { return; } // Simply enqueue the scripts; All of them have been registered. if ( 'yes' === get_option( 'woocommerce_enable_ajax_add_to_cart' ) ) { wp_enqueue_script( 'wc-add-to-cart' ); } if ( current_theme_supports( 'wc-product-gallery-zoom' ) ) { wp_enqueue_script( 'zoom' ); } if ( current_theme_supports( 'wc-product-gallery-slider' ) ) { wp_enqueue_script( 'flexslider' ); } if ( current_theme_supports( 'wc-product-gallery-lightbox' ) ) { wp_enqueue_script( 'photoswipe-ui-default' ); wp_enqueue_style( 'photoswipe-default-skin' ); add_action( 'wp_footer', 'woocommerce_photoswipe' ); } wp_enqueue_script( 'wc-single-product' ); if ( 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) ) { $ua = strtolower( wc_get_user_agent() ); // Exclude common bots from geolocation by user agent. if ( ! strstr( $ua, 'bot' ) && ! strstr( $ua, 'spider' ) && ! strstr( $ua, 'crawl' ) ) { wp_enqueue_script( 'wc-geolocation' ); } } wp_enqueue_script( 'woocommerce' ); wp_enqueue_script( 'wc-cart-fragments' ); wp_enqueue_script( 'wc-checkout' ); wp_enqueue_script( 'select2' ); wp_enqueue_script( 'selectWoo' ); wp_enqueue_style( 'select2' ); // Enqueue style. $wc_styles = WC_Frontend_Scripts::get_styles(); /* * Since $wc_styles is passed in to `woocommerce_enqueue_styles` filter, * ensure that the value is array. * * @see https://github.com/elegantthemes/divi-builder/issues/1268 */ if ( ! is_array( $wc_styles ) ) { return; } foreach ( $wc_styles as $style_handle => $wc_style ) { if ( ! isset( $wc_style['has_rtl'] ) ) { $wc_style['has_rtl'] = false; } wp_enqueue_style( $style_handle, $wc_style['src'], $wc_style['deps'], $wc_style['version'], $wc_style['media'], $wc_style['has_rtl'] ); } } /** * Add WooCommerce body class name on non `product` CPT builder page * * @param array $classes CSS class names. * * @return array * @since 3.29 */ function et_builder_wc_add_body_class( $classes ) { if ( et_builder_wc_is_non_product_post_type() || is_et_pb_preview() ) { $classes[] = 'woocommerce'; $classes[] = 'woocommerce-page'; } return $classes; } /** * Add product class name on inner content wrapper page on non `product` CPT builder page with woocommerce modules * And on Product posts * * @param array $classes Product class names. * * @return array * @since 3.29 */ function et_builder_wc_add_inner_content_class( $classes ) { // The class is required on any post with woocommerce modules and on product pages. if ( et_builder_wc_is_non_product_post_type() || is_product() || is_et_pb_preview() ) { $classes[] = 'product'; } return $classes; } /** * Add WooCommerce class names on Divi Shop Page (not WooCommerce Shop). * * @since 4.0.7 * * @param array $classes Array of Classes. * * @return array */ function et_builder_wc_add_outer_content_class( $classes ) { $body_classes = get_body_class(); // Add Class only to WooCommerce Shop page if built using Divi (i.e. Divi Shop page). if ( ! ( function_exists( 'is_shop' ) && is_shop() ) ) { return $classes; } // Add Class only when the WooCommerce Shop page is built using Divi. if ( ! et_builder_wc_is_non_product_post_type() ) { return $classes; } // Precautionary check: $body_classes should always be an array. if ( ! is_array( $body_classes ) ) { return $classes; } // Add Class only when the
tag does not contain them. $woocommerce_classes = array( 'woocommerce', 'woocommerce-page' ); $common_classes = array_intersect( $body_classes, array( 'woocommerce', 'woocommerce-page', ) ); if ( is_array( $common_classes ) && count( $woocommerce_classes ) === count( $common_classes ) ) { return $classes; } // Precautionary check: $classes should always be an array. if ( ! is_array( $classes ) ) { return $classes; } $classes[] = 'woocommerce'; $classes[] = 'woocommerce-page'; return $classes; } /** * Sets the Product page layout post meta on two occurrences. * * They are 1) On WP Admin Publish/Update post 2) On VB Save. * * @since 4.14.0 Remove ET_BUILDER_WC_PRODUCT_PAGE_LAYOUT_META_KEY meta key on non-product post types. * Also move `since` section above `param` section. * @since 3.29 * * @param int $post_id Post ID. */ function et_builder_set_product_page_layout_meta( $post_id ) { $post = get_post( $post_id ); if ( ! $post ) { return; } /* * The Product page layout post meta adds no meaning to the Post when the Builder is not used. * Hence the meta key/value is removed, when the Builder is turned off. */ if ( ! et_pb_is_pagebuilder_used( $post_id ) ) { delete_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_PAGE_LAYOUT_META_KEY ); return; } // The meta key is to be used only on Product post types. // Hence remove the meta if exists on other post types. $is_non_product_post_type = 'product' !== $post->post_type; if ( $is_non_product_post_type ) { // Returns FALSE when no meta key is found. delete_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_PAGE_LAYOUT_META_KEY ); return; } // Do not update Product page layout post meta when it contains a value. $product_page_layout = get_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_PAGE_LAYOUT_META_KEY, true ); if ( $product_page_layout ) { return; } $product_page_layout = et_get_option( 'et_pb_woocommerce_page_layout', 'et_build_from_scratch' ); update_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_PAGE_LAYOUT_META_KEY, sanitize_text_field( $product_page_layout ) ); } /** * Sets the Product content status as modified during VB save. * * This avoids setting the default WooCommerce Modules layout more than once. * * @link https://github.com/elegantthemes/Divi/issues/16420 * * @param int $post_id Post ID. */ function et_builder_set_product_content_status( $post_id ) { if ( 0 === absint( $post_id ) ) { return; } if ( 'product' !== get_post_type( $post_id ) || 'modified' === get_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_PAGE_CONTENT_STATUS_META_KEY, true ) ) { return; } update_post_meta( $post_id, ET_BUILDER_WC_PRODUCT_PAGE_CONTENT_STATUS_META_KEY, 'modified' ); } /** * Gets Woocommerce Tabs for the given Product ID. * * @since 4.4.2 */ function et_builder_get_woocommerce_tabs() { // Nonce verification. et_core_security_check( 'edit_posts', 'et_builder_get_woocommerce_tabs', 'nonce' ); $_ = et_(); $product_id = $_->array_get( $_POST, 'product', 0 ); if ( null === $product_id || ! et_is_woocommerce_plugin_active() ) { wp_send_json_error(); } // Allow Latest Product ID which is a string 'latest'. // `This Product` tabs are defined in et_fb_current_page_params(). if ( ! in_array( $product_id, array( 'current', 'latest' ), true ) && 0 === absint( $product_id ) ) { wp_send_json_error(); } $tabs = ET_Builder_Module_Woocommerce_Tabs::get_tabs( array( 'product' => $product_id ) ); wp_send_json_success( $tabs ); } /** * Returns alternative hook to make Woo Extra Product Options display fields in FE when TB is * enabled. * * - The Woo Extra Product Options addon does not display the extra fields on the FE. * - This is because the original hook i.e. `woocommerce_before_single_product` in the plugin * is not triggered when TB is enabled. * - Hence return a suitable hook that is fired for all types of Products i.e. Simple, Variable, * etc. * * @param string $hook Hook name. * * @return string WooCommerce Hook that is being fired on TB enabled Product pages. * @see WEPOF_Product_Options_Frontend::define_public_hooks() * * @since 4.0.9 */ function et_builder_trigger_extra_product_options( $hook ) { return 'woocommerce_before_add_to_cart_form'; } /** * Strip Builder shortcodes to avoid nested parsing. * * @see https://github.com/elegantthemes/Divi/issues/18682 * * @param string $content Post content. * * @since 4.3.3 * * @return string */ function et_builder_avoid_nested_shortcode_parsing( $content ) { if ( is_et_pb_preview() ) { return $content; } // Strip shortcodes only on non-builder pages that contain Builder shortcodes. if ( et_pb_is_pagebuilder_used( get_the_ID() ) ) { return $content; } // WooCommerce layout loads when builder is not enabled. // So strip builder shortcodes from Post content. if ( function_exists( 'is_product' ) && is_product() ) { return et_strip_shortcodes( $content ); } // Strip builder shortcodes from non-product pages. // Only Tabs shortcode is checked since that causes nested rendering. if ( has_shortcode( $content, 'et_pb_wc_tabs' ) ) { return et_strip_shortcodes( $content ); } return $content; } /** * Parses Product description to * * - converts any [embed][/embed] shortcode to its respective HTML. * - strips `et_` shortcodes to avoid nested rendering in Woo Tabs module. * - addstag to keep the paragraph sanity. * - runs other shortcodes if any using do_shortcode. * * @since 4.4.1 * * @param string $description Product description i.e. Post content. * * @return string */ function et_builder_wc_parse_description( $description ) { if ( ! is_string( $description ) ) { return $description; } global $wp_embed; $parsed_description = et_strip_shortcodes( $description ); $parsed_description = $wp_embed->run_shortcode( $parsed_description ); $parsed_description = do_shortcode( $parsed_description ); $parsed_description = wpautop( $parsed_description ); return $parsed_description; } /** * Deletes ET_BUILDER_WC_PRODUCT_PAGE_CONTENT_STATUS_META_KEY when Builder is OFF. * * The deletion allows switching between Divi Builder and the GB builder smoothly. * * @link https://github.com/elegantthemes/Divi/issues/22477 * * @since 4.14.0 * * @param WP_Post $post Post Object. */ function et_builder_wc_delete_post_meta( $post ) { if ( ! ( $post instanceof WP_Post ) ) { return; } if ( et_pb_is_pagebuilder_used( $post->ID ) ) { return; } delete_post_meta( $post->ID, ET_BUILDER_WC_PRODUCT_PAGE_CONTENT_STATUS_META_KEY ); } /** * Adds the Preview class to the wrapper. * * @param string $maybe_class_string Classnames string. * @return string */ function et_builder_wc_add_preview_wrap_class( $maybe_class_string ) { if ( ! is_string( $maybe_class_string ) ) { return $maybe_class_string; } $classes = explode( ' ', $maybe_class_string ); $classes[] = 'product'; return implode( ' ', $classes ); } /** * Entry point for the woocommerce-modules.php file. * * @since 3.29 */ function et_builder_wc_init() { // global $post won't be available with `after_setup_theme` hook and hence `wp` hook is used. add_action( 'wp', 'et_builder_wc_override_default_layout' ); // Add WooCommerce class names on non-`product` CPT which uses builder. add_filter( 'body_class', 'et_builder_wc_add_body_class' ); add_filter( 'et_builder_inner_content_class', 'et_builder_wc_add_inner_content_class' ); add_filter( 'et_pb_preview_wrap_class', 'et_builder_wc_add_preview_wrap_class' ); add_filter( 'et_builder_outer_content_class', 'et_builder_wc_add_outer_content_class' ); // Load WooCommerce related scripts. add_action( 'wp_enqueue_scripts', 'et_builder_wc_load_scripts', 15 ); add_filter( 'et_builder_skip_content_activation', 'et_builder_wc_skip_initial_content', 10, 2 ); // Show Product Content dropdown settings under // Divi Theme Options ⟶ Builder ⟶ Post TYpe Integration. add_filter( 'et_builder_settings_definitions', 'et_builder_wc_add_settings' ); /** * Adds the metabox only to Product post type. * * This is achieved using the post type hook - add_meta_boxes_{post_type}. * * @see https://codex.wordpress.org/Plugin_API/Action_Reference/add_meta_boxes * * @since 3.29 */ add_action( 'add_meta_boxes_product', 'et_builder_wc_long_description_metabox_register' ); // Saves the long description metabox data. // Since `et_pb_metabox_settings_save_details()` already uses `save_post` hook // to save `_et_pb_old_content` post meta, // we use this additional hook `et_pb_old_content_updated`. add_action( 'et_pb_old_content_updated', 'et_builder_wc_long_description_metabox_save', 10, 3 ); /* * 01. Sets the initial Content when `Use Divi Builder` button is clicked * in the Admin dashboard. * 02. Sets the initial Content when `Enable Visual Builder` is clicked. */ add_filter( 'et_fb_load_raw_post_content', 'et_builder_wc_set_prefilled_page_content', 10, 2 ); add_action( 'et_save_post', 'et_builder_set_product_page_layout_meta' ); /* * Set the Product modified status as modified upon save to make sure default layout is not * loaded more than one time. * * @see https://github.com/elegantthemes/Divi/issues/16420 */ add_action( 'et_update_post', 'et_builder_wc_set_page_content_status' ); /* * Handle get Woocommerce tabs AJAX call initiated by Tabs checkbox in settings modal. */ add_action( 'wp_ajax_et_builder_get_woocommerce_tabs', 'et_builder_get_woocommerce_tabs' ); /* * Fix Woo Extra Product Options addon compatibility. * @see https://github.com/elegantthemes/Divi/issues/17909 */ add_filter( 'thwepof_hook_name_before_single_product', 'et_builder_trigger_extra_product_options' ); /* * Fix nested parsing on non-builder product pages w/ shortcode content. * @see https://github.com/elegantthemes/Divi/issues/18682 */ add_filter( 'the_content', 'et_builder_avoid_nested_shortcode_parsing' ); add_filter( 'et_builder_wc_description', 'et_builder_wc_parse_description' ); add_filter( 'template_redirect', 'et_builder_wc_template_redirect', 9 ); /* * Delete `_et_pb_woo_page_content_status` post meta when Divi Builder is off * when using GB editor. * * The latest value of `_et_pb_use_builder` post meta is only available in * `rest_after_insert_page` and NOT in `rest_insert_page` hook. * * This action is documented in * wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ add_action( 'rest_after_insert_page', 'et_builder_wc_delete_post_meta' ); add_filter( 'woocommerce_checkout_redirect_empty_cart', 'et_builder_stop_cart_redirect_while_enabling_builder' ); /* * `wp_loaded` is used intentionally because * `get_cart()` should not be called before wp_loaded hook. */ add_action( 'wp_loaded', 'et_builder_handle_shipping_calculator_update_btn_click' ); /* * In the case of dynamic module framework's shortcode manager * we need to fire this hook on its own, */ if ( ! et_builder_should_load_all_module_data() ) { add_action( 'et_builder_module_lazy_shortcodes_registered', [ 'ET_Builder_Module_Woocommerce_Cart_Notice', 'disable_default_notice', ] ); add_action( 'et_builder_module_lazy_shortcodes_registered', [ 'ET_Builder_Module_Woocommerce_Checkout_Additional_Info', 'maybe_invoke_woocommerce_hooks', ] ); } // Relocate WC single product summary hooks to any suitable modules. add_action( 'et_builder_ready', 'et_builder_wc_relocate_single_product_summary' ); } et_builder_wc_init();