string for default image placeholder * * @since 4.0.10 * * @return string */ function et_builder_wc_placeholder_img() { return sprintf( '2$s', et_core_esc_attr( 'placeholder', ET_BUILDER_PLACEHOLDER_LANDSCAPE_IMAGE_DATA ), 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_initial_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_initial_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; } /** * 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', ); 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(); if ( $is_tb ) { // 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 ) { $is_latest_product = 'latest' === $args['product']; $is_current_product_page = 'current' === $args['product']; 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( $args['product'] ); } 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( $args['product'] ) ) { $product_id = $args['product']; } else { // Fallback to Latest product if saved product ID doesn't exist. $product_id = ET_Builder_Module_Helper_Woocommerce_Modules::get_product_id( 'latest' ); } } 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': // 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 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': 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': // @phpcs:ignore Generic.PHP.ForbiddenFunctions.Found call_user_func( $function_name, wc_add_to_cart_message( $product->get_id(), false, true ) ); break; case 'wc_print_notices': // 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 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_upsell_display': $order = isset( $args['order'] ) ? $args['order'] : ''; // @phpcs:ignore Generic.PHP.ForbiddenFunctions.Found call_user_func( $function_name, '', '', '', $order ); break; default: // @phpcs:ignore Generic.PHP.ForbiddenFunctions.Found call_user_func( $function_name ); } $output = ob_get_clean(); // Reset original product variable to global $product. if ( $is_tb ) { 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 ); } /** * 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_initial_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() { 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_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 ) { 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' ); // 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() ) { $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() ) { $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. * * @param int $post_id Post id. * * @since 3.29 */ 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; } // 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 ) { // 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. * - adds

tag 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; } /** * 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_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_initial_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_set_product_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' ); /* * 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', ] ); } } et_builder_wc_init();