whoami7 - Manager
:
/
home
/
ezdajrnh
/
www
/
wp-content
/
plugins
/
facebook-for-woocommerce
/
includes
/
Upload File:
files >> //home/ezdajrnh/www/wp-content/plugins/facebook-for-woocommerce/includes/Admin.php
<?php /** * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. * * @package FacebookCommerce */ namespace WooCommerce\Facebook; use WooCommerce\Facebook\Admin\Enhanced_Catalog_Attribute_Fields; use WooCommerce\Facebook\Framework\Helper; use WooCommerce\Facebook\ProductAttributeMapper; use WooCommerce\Facebook\RolloutSwitches; use Automattic\WooCommerce\Utilities\OrderUtil; defined( 'ABSPATH' ) || exit; /** * Admin handler. * * @since 1.10.0 */ class Admin { /** @var string the "sync and show" sync mode slug */ const SYNC_MODE_SYNC_AND_SHOW = 'sync_and_show'; /** @var string the "sync and show" sync mode slug */ const SYNC_MODE_SYNC_AND_HIDE = 'sync_and_hide'; /** @var string the "sync disabled" sync mode slug */ const SYNC_MODE_SYNC_DISABLED = 'sync_disabled'; /** @var string the "fb_sync_enabled" sync mode slug */ const INCLUDE_FACEBOOK_SYNC = 'fb_sync_enabled'; /** @var string the "fb_sync_disabled" sync mode slug */ const EXCLUDE_FACEBOOK_SYNC = 'fb_sync_disabled'; /** @var string the "include" sync mode for bulk edit */ const BULK_EDIT_SYNC = 'bulk_edit_sync'; /** @var string the "exclude" sync mode for bulk edit */ const BULK_EDIT_DELETE = 'bulk_edit_delete'; /** @var Product_Categories the product category admin handler */ protected $product_categories; /** @var array screens ids where to include scripts */ protected $screen_ids = []; /** * Admin constructor. * * @since 1.10.0 */ public function __construct() { $order_screen_id = class_exists( OrderUtil::class ) ? OrderUtil::get_order_admin_screen() : 'shop_order'; $this->screen_ids = [ 'product', 'edit-product', 'woocommerce_page_wc-facebook', 'marketing_page_wc-facebook', 'edit-product_cat', $order_screen_id, ]; // enqueue admin scripts add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); $plugin = facebook_for_woocommerce(); // only alter the admin UI if the plugin is connected to Facebook and ready to sync products if ( ! $plugin->get_connection_handler()->is_connected() || ! $plugin->get_integration()->get_product_catalog_id() ) { return; } $this->product_categories = new Admin\Product_Categories(); // add a modal in admin product pages add_action( 'admin_footer', array( $this, 'render_modal_template' ) ); add_action( 'admin_footer', array( $this, 'add_tab_switch_script' ) ); // add admin notice to inform that disabled products may need to be deleted manually add_action( 'admin_notices', array( $this, 'maybe_show_product_disabled_sync_notice' ) ); // add admin notice if the user is enabling sync for virtual products using the bulk action add_action( 'admin_notices', array( $this, 'maybe_add_enabling_virtual_products_sync_notice' ) ); add_filter( 'request', array( $this, 'filter_virtual_products_affected_enabling_sync' ) ); // add admin notice to inform sync mode has been automatically set to Sync and hide for virtual products and variations add_action( 'admin_notices', array( $this, 'add_handled_virtual_products_variations_notice' ) ); // add columns for displaying Facebook sync enabled/disabled and catalog visibility status add_filter( 'manage_product_posts_columns', array( $this, 'add_product_list_table_columns' ) ); add_action( 'manage_product_posts_custom_column', array( $this, 'add_product_list_table_columns_content' ) ); // add input to filter products by Facebook sync enabled add_action( 'restrict_manage_posts', array( $this, 'add_products_by_sync_enabled_input_filter' ), 40 ); add_filter( 'request', array( $this, 'filter_products_by_sync_enabled' ) ); // add bulk actions to manage products sync add_action( 'woocommerce_product_bulk_edit_end', array( $this, 'add_facebook_sync_bulk_edit_dropdown_at_bottom' ) ); add_action( 'woocommerce_product_bulk_edit_save', array( $this, 'handle_products_sync_bulk_actions' ), 10, 1 ); // add Product data tab add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_product_settings_tab' ) ); add_action( 'woocommerce_product_data_panels', array( $this, 'add_product_settings_tab_content' ) ); // add Variation edit fields add_action( 'woocommerce_product_after_variable_attributes', array( $this, 'add_product_variation_edit_fields' ), 10, 3 ); add_action( 'woocommerce_save_product_variation', array( $this, 'save_product_variation_edit_fields' ), 10, 2 ); add_action( 'wp_ajax_get_facebook_product_data', array( $this, 'ajax_get_facebook_product_data' ) ); add_action( 'wp_ajax_sync_facebook_attributes', array( $this, 'ajax_sync_facebook_attributes' ) ); } /** * Enqueues admin scripts. * * @internal * * @since 1.10.0 */ public function enqueue_scripts() { global $current_screen; if ( isset( $current_screen->id ) ) { if ( in_array( $current_screen->id, $this->screen_ids, true ) || facebook_for_woocommerce()->is_plugin_settings() ) { // enqueue modal functions wp_enqueue_script( 'facebook-for-woocommerce-modal', facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/modal.js', array( 'jquery', 'wc-backbone-modal', 'jquery-blockui' ), \WC_Facebookcommerce::PLUGIN_VERSION, false ); // enqueue google product category select wp_enqueue_script( 'wc-facebook-google-product-category-fields', facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/google-product-category-fields.js', array( 'jquery' ), \WC_Facebookcommerce::PLUGIN_VERSION, false ); wp_localize_script( 'wc-facebook-google-product-category-fields', 'facebook_for_woocommerce_google_product_category', array( 'i18n' => array( 'top_level_dropdown_placeholder' => __( 'Search main categories...', 'facebook-for-woocommerce' ), 'second_level_empty_dropdown_placeholder' => __( 'Choose a main category first', 'facebook-for-woocommerce' ), 'general_dropdown_placeholder' => __( 'Choose a category', 'facebook-for-woocommerce' ), ), ) ); } if ( 'product' === $current_screen->id || 'edit-product' === $current_screen->id ) { wp_enqueue_style( 'facebook-for-woocommerce-products-admin', facebook_for_woocommerce()->get_plugin_url() . '/assets/css/admin/facebook-for-woocommerce-products-admin.css', [], \WC_Facebookcommerce::PLUGIN_VERSION ); wp_enqueue_script( 'facebook-for-woocommerce-products-admin', facebook_for_woocommerce()->get_asset_build_dir_url() . '/admin/products-admin.js', [ 'jquery', 'wc-backbone-modal', 'jquery-blockui', 'facebook-for-woocommerce-modal' ], \WC_Facebookcommerce::PLUGIN_VERSION, false ); wp_localize_script( 'facebook-for-woocommerce-products-admin', 'facebook_for_woocommerce_products_admin', [ 'ajax_url' => admin_url( 'admin-ajax.php' ), 'enhanced_attribute_optional_selector' => Enhanced_Catalog_Attribute_Fields::FIELD_ENHANCED_CATALOG_ATTRIBUTE_PREFIX . Enhanced_Catalog_Attribute_Fields::OPTIONAL_SELECTOR_KEY, 'enhanced_attribute_page_type_edit_category' => Enhanced_Catalog_Attribute_Fields::PAGE_TYPE_EDIT_CATEGORY, 'enhanced_attribute_page_type_add_category' => Enhanced_Catalog_Attribute_Fields::PAGE_TYPE_ADD_CATEGORY, 'enhanced_attribute_page_type_edit_product' => Enhanced_Catalog_Attribute_Fields::PAGE_TYPE_EDIT_PRODUCT, 'is_product_published' => $this->is_current_product_published(), 'is_sync_enabled_for_product' => $this->is_sync_enabled_for_current_product(), 'set_product_visibility_nonce' => wp_create_nonce( 'set-products-visibility' ), 'set_product_sync_prompt_nonce' => wp_create_nonce( 'set-product-sync-prompt' ), 'set_product_sync_bulk_action_prompt_nonce' => wp_create_nonce( 'set-product-sync-bulk-action-prompt' ), 'product_not_ready_modal_message' => $this->get_product_not_ready_modal_message(), 'product_not_ready_modal_buttons' => $this->get_product_not_ready_modal_buttons(), 'product_removed_from_sync_field_id' => '#' . \WC_Facebook_Product::FB_REMOVE_FROM_SYNC, 'i18n' => [ 'missing_google_product_category_message' => __( 'Please enter a Google product category and at least one sub-category to sell this product on Instagram.', 'facebook-for-woocommerce' ), ], ] ); }//end if if ( facebook_for_woocommerce()->is_plugin_settings() ) { wp_enqueue_style( 'woocommerce_admin_styles' ); wp_enqueue_script( 'wc-enhanced-select' ); } }//end if } /** * Determines whether sync is enabled for the current product. * * @since 2.0.5 * * @return bool */ private function is_sync_enabled_for_current_product() { global $post; $product = wc_get_product( $post ); if ( ! $product instanceof \WC_Product ) { return false; } return Products::is_sync_enabled_for_product( $product ); } /** * Determines whether the current product is published. * * @since 2.6.15 * * @return bool */ private function is_current_product_published() { global $post; $product = wc_get_product( $post ); if ( ! $product instanceof \WC_Product ) { return false; } return 'publish' === $product->get_status(); } /** * Gets the markup for the message used in the product not ready modal. * * @since 2.1.0 * * @return string */ private function get_product_not_ready_modal_message() { ob_start(); ?> <p><?php esc_html_e( 'To sell this product on Instagram, please ensure it meets the following requirements:', 'facebook-for-woocommerce' ); ?></p> <ul class="ul-disc"> <li><?php esc_html_e( 'Has a price defined', 'facebook-for-woocommerce' ); ?></li> <li> <?php echo esc_html( sprintf( /* translators: Placeholders: %1$s - <strong> opening HTML tag, %2$s - </strong> closing HTML tag */ __( 'Has %1$sManage Stock%2$s enabled on the %1$sInventory%2$s tab', 'facebook-for-woocommerce' ), '<strong>', '</strong>' ) ); ?> </li> <li> <?php echo esc_html( sprintf( /* translators: Placeholders: %1$s - <strong> opening HTML tag, %2$s - </strong> closing HTML tag */ __( 'Has the %1$sFacebook Sync%2$s setting set to "Sync and show" or "Sync and hide"', 'facebook-for-woocommerce' ), '<strong>', '</strong>' ) ); ?> </li> </ul> <?php return ob_get_clean(); } /** * Gets the markup for the buttons used in the product not ready modal. * * @since 2.1.0 * * @return string */ private function get_product_not_ready_modal_buttons() { ob_start(); ?> <button id="btn-ok" class="button button-large button-primary" ><?php esc_html_e( 'Close', 'facebook-for-woocommerce' ); ?></button> <?php return ob_get_clean(); } /** * Gets the product category admin handler instance. * * @since 2.1.0 * * @return Product_Categories */ public function get_product_categories_handler() { return $this->product_categories; } /** * Adds Facebook-related columns in the products edit screen. * * @internal * * @since 1.10.0 * * @param array $columns array of keys and labels * @return array */ public function add_product_list_table_columns( $columns ) { $columns['facebook_sync'] = __( 'Synced to Meta catalog', 'facebook-for-woocommerce' ); return $columns; } /** * Outputs sync information for products in the edit screen. * * @internal * * @since 1.10.0 * * @param string $column the current column in the posts table */ public function add_product_list_table_columns_content( $column ) { global $post; if ( 'facebook_sync' !== $column ) { return; } $product = wc_get_product( $post ); $should_sync = false; $no_sync_reason = ''; if ( $product instanceof \WC_Product ) { try { facebook_for_woocommerce()->get_product_sync_validator( $product )->validate(); $should_sync = true; } catch ( \Exception $e ) { $no_sync_reason = $e->getMessage(); } } if ( $should_sync ) { esc_html_e( 'Synced', 'facebook-for-woocommerce' ); } else { esc_html_e( 'Not synced', 'facebook-for-woocommerce' ); if ( ! empty( $no_sync_reason ) ) { echo wc_help_tip( $no_sync_reason ); } } } /** * Adds a dropdown input to let shop managers filter products by sync setting. * * @internal * * @since 1.10.0 */ public function add_products_by_sync_enabled_input_filter() { global $typenow; if ( 'product' !== $typenow ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $choice = isset( $_GET['fb_sync_enabled'] ) ? (string) sanitize_text_field( wp_unslash( $_GET['fb_sync_enabled'] ) ) : ''; ?> <select name="fb_sync_enabled"> <option value="" <?php selected( $choice, '' ); ?>><?php esc_html_e( 'Filter by synced to Meta', 'facebook-for-woocommerce' ); ?></option> <option value="<?php echo esc_attr( self::INCLUDE_FACEBOOK_SYNC ); ?>" <?php selected( $choice, self::INCLUDE_FACEBOOK_SYNC ); ?>><?php esc_html_e( 'Synced', 'facebook-for-woocommerce' ); ?></option> <option value="<?php echo esc_attr( self::EXCLUDE_FACEBOOK_SYNC ); ?>" <?php selected( $choice, self::EXCLUDE_FACEBOOK_SYNC ); ?>><?php esc_html_e( 'Not synced', 'facebook-for-woocommerce' ); ?></option> </select> <?php } /** * Adds a dropdown input to Include or Exclude product in Facebook Bulk Sync. * * @internal */ public function add_facebook_sync_bulk_edit_dropdown_at_bottom() { global $typenow; if ( 'product' !== $typenow ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $choice = isset( $_GET['facebook_bulk_sync_options'] ) ? (string) sanitize_text_field( wp_unslash( $_GET['facebook_bulk_sync_options'] ) ) : ''; ?> <label> <span class="title"><?php esc_html_e( 'Sync to Meta catalog', 'facebook-for-woocommerce' ); ?></span> <span class="input-text-wrap"> <select class="facebook_bulk_sync_options" name="facebook_bulk_sync_options"> <option value=""> <?php esc_html_e( '— No Change —', 'facebook-for-woocommerce' ); ?></option>; <option value="<?php echo esc_attr( self::BULK_EDIT_SYNC ); ?>" <?php selected( $choice, self::BULK_EDIT_SYNC ); ?>><?php esc_html_e( 'Sync', 'facebook-for-woocommerce' ); ?></option> <option value="<?php echo esc_attr( self::BULK_EDIT_DELETE ); ?>" <?php selected( $choice, self::BULK_EDIT_DELETE ); ?>><?php esc_html_e( 'Do not sync', 'facebook-for-woocommerce' ); ?></option> </select> </span> </label> <?php } /** * Filters products by Facebook sync setting. * * @internal * * @since 1.10.0 * * @param array $query_vars product query vars for the edit screen * @return array */ public function filter_products_by_sync_enabled( $query_vars ) { $valid_values = array( self::INCLUDE_FACEBOOK_SYNC, self::EXCLUDE_FACEBOOK_SYNC, ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_REQUEST['fb_sync_enabled'] ) && in_array( $_REQUEST['fb_sync_enabled'], $valid_values, true ) ) { // store original meta query $original_meta_query = ! empty( $query_vars['meta_query'] ) ? $query_vars['meta_query'] : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $filter_value = wc_clean( wp_unslash( $_REQUEST['fb_sync_enabled'] ) ); // by default use an "AND" clause if multiple conditions exist for a meta query if ( ! empty( $query_vars['meta_query'] ) ) { $query_vars['meta_query']['relation'] = 'AND'; } else { $query_vars['meta_query'] = []; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query } if ( self::INCLUDE_FACEBOOK_SYNC === $filter_value ) { $query_vars = $this->add_query_vars_to_find_products_with_sync_enabled( $query_vars ); $found_ids = get_posts( array_merge( $query_vars, array( 'fields' => 'ids' ) ) ); $found_products = empty( $found_ids ) ? [] : wc_get_products( array( 'limit' => -1, 'include' => $found_ids, ) ); /** @var \WC_Product[] $found_products */ foreach ( $found_products as $product ) { try { facebook_for_woocommerce()->get_product_sync_validator( $product )->validate(); } catch ( \Exception $e ) { $exclude_products[] = $product->get_id(); } } if ( ! empty( $exclude_products ) ) { if ( ! empty( $query_vars['post__not_in'] ) ) { $query_vars['post__not_in'] = array_merge( $query_vars['post__not_in'], $exclude_products ); } else { $query_vars['post__not_in'] = $exclude_products; } } /** * Showing only published products as private ones are not synced */ $query_vars['post_status'] = 'publish'; } else { $query_vars = $this->add_query_vars_to_find_products_with_sync_disabled( $query_vars ); } } if ( isset( $query_vars['meta_query'] ) && empty( $query_vars['meta_query'] ) ) { unset( $query_vars['meta_query'] ); } return $query_vars; } /** * Adds query vars to limit the results to products that have sync enabled. * * @since 1.10.0 * * @param array $query_vars * @return array */ private function add_query_vars_to_find_products_with_sync_enabled( array $query_vars ) { $meta_query = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'OR', array( 'key' => Products::get_product_sync_meta_key(), 'value' => 'yes', ), array( 'key' => Products::get_product_sync_meta_key(), 'compare' => 'NOT EXISTS', ), ); if ( empty( $query_vars['meta_query'] ) ) { $query_vars['meta_query'] = $meta_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query } elseif ( is_array( $query_vars['meta_query'] ) ) { $original_meta_query = $query_vars['meta_query']; $query_vars['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'AND', $original_meta_query, $meta_query, ); } // check whether the product belongs to an excluded product category or tag $query_vars = $this->maybe_add_tax_query_for_excluded_taxonomies( $query_vars ); return $query_vars; } /** * Adds query vars to limit the results to products that have sync disabled. * * @since 3.5.5 * * @param array $query_vars * @return array */ private function add_query_vars_to_find_products_with_sync_disabled( array $query_vars ) { $meta_query = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => Products::get_product_sync_meta_key(), 'value' => 'no', ), ); if ( empty( $query_vars['meta_query'] ) ) { $query_vars['meta_query'] = $meta_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query } elseif ( is_array( $query_vars['meta_query'] ) ) { $original_meta_query = $query_vars['meta_query']; $query_vars['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'AND', $original_meta_query, $meta_query, ); } return $query_vars; } /** * Adds a tax query to filter in/out products in excluded product categories and product tags. * * @since 1.10.0 * * @param array $query_vars product query vars for the edit screen * @param bool $in whether we want to return products in excluded categories and tags or not * @return array */ private function maybe_add_tax_query_for_excluded_taxonomies( $query_vars, $in = false ) { $integration = facebook_for_woocommerce()->get_integration(); if ( $integration ) { $tax_query = []; $excluded_categories_ids = $integration->get_excluded_product_category_ids(); if ( $excluded_categories_ids ) { $tax_query[] = array( 'taxonomy' => 'product_cat', 'terms' => $excluded_categories_ids, 'field' => 'term_id', 'operator' => $in ? 'IN' : 'NOT IN', ); } $excluded_tags_ids = $integration->get_excluded_product_tag_ids(); if ( $excluded_tags_ids ) { $tax_query[] = array( 'taxonomy' => 'product_tag', 'terms' => $excluded_tags_ids, 'field' => 'term_id', 'operator' => $in ? 'IN' : 'NOT IN', ); } if ( count( $tax_query ) > 1 ) { $tax_query['relation'] = $in ? 'OR' : 'AND'; } if ( $tax_query && empty( $query_vars['tax_query'] ) ) { $query_vars['tax_query'] = $tax_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query } elseif ( $tax_query && is_array( $query_vars ) ) { $query_vars['tax_query'][] = $tax_query; } }//end if return $query_vars; } /** * Adds bulk actions in the products edit screen. * * @internal * * @since 1.10.0 * * @param array $bulk_actions array of bulk action keys and labels * @return array */ public function add_products_sync_bulk_actions( $bulk_actions ) { $bulk_actions['facebook_include'] = __( 'Include in Facebook sync', 'facebook-for-woocommerce' ); $bulk_actions['facebook_exclude'] = __( 'Exclude from Facebook sync', 'facebook-for-woocommerce' ); return $bulk_actions; } /** * Handles a Facebook product sync bulk action. * Called every time for a product * * @internal * * @param string $product_edit the product metadata that is being edited. */ public function handle_products_sync_bulk_actions( $product_edit ) { $sync_mode = isset( $_GET['facebook_bulk_sync_options'] ) ? (string) sanitize_text_field( wp_unslash( $_GET['facebook_bulk_sync_options'] ) ) : null; if ( $sync_mode ) { /** @var \WC_Product[] $enabling_sync_virtual_products virtual products that are being included */ $enabling_sync_virtual_products = []; /** @var \WC_Product_Variation[] $enabling_sync_virtual_variations virtual variations that are being included */ $enabling_sync_virtual_variations = []; /** @var \WC_Product $product to store the product meta data */ $product = wc_get_product( $product_edit ); if ( $product && $this::BULK_EDIT_SYNC === $sync_mode ) { if ( $product->is_virtual() && ! Products::is_sync_enabled_for_product( $product ) ) { $enabling_sync_virtual_products[ $product->get_id() ] = $product; } elseif ( $product->is_type( 'variable' ) ) { // collect the virtual variations foreach ( $product->get_children() as $variation_id ) { $variation = wc_get_product( $variation_id ); if ( $variation && $variation->is_virtual() && ! Products::is_sync_enabled_for_product( $variation ) ) { $enabling_sync_virtual_variations[ $variation->get_id() ] = $variation; } }//end foreach if ( ! empty( $enabling_sync_virtual_variations ) ) { $enabling_sync_virtual_products[ $product->get_id() ] = $product; }//end if }//end if }//end if if ( ! empty( $enabling_sync_virtual_products ) || ! empty( $enabling_sync_virtual_variations ) ) { // display notice if enabling sync for virtual products or variations set_transient( 'wc_' . facebook_for_woocommerce()->get_id() . '_enabling_virtual_products_sync_show_notice_' . get_current_user_id(), true, 15 * MINUTE_IN_SECONDS ); set_transient( 'wc_' . facebook_for_woocommerce()->get_id() . '_enabling_virtual_products_sync_affected_products_' . get_current_user_id(), array_keys( $enabling_sync_virtual_products ), 15 * MINUTE_IN_SECONDS ); // set visibility for virtual products foreach ( $enabling_sync_virtual_products as $product ) { // do not set visibility for variable products if ( ! $product->is_type( 'variable' ) ) { Products::set_product_visibility( $product, false ); } } // set visibility for virtual variations foreach ( $enabling_sync_virtual_variations as $variation ) { Products::set_product_visibility( $variation, false ); } }//end if $products[] = $product; if ( $this::BULK_EDIT_SYNC === $sync_mode ) { Products::enable_sync_for_products( $products ); $this->resync_products( $products ); } elseif ( $this::BULK_EDIT_DELETE === $sync_mode ) { Products::disable_sync_for_products( $products ); } } //end if } /** * Re-syncs the given products. * * @since 2.0.0 * * @param \WC_Product[] $products Array of product objects to resync */ private function resync_products( array $products ) { $integration = facebook_for_woocommerce()->get_integration(); // re-sync each product foreach ( $products as $product ) { if ( $product->is_type( 'variable' ) ) { // create product group and schedule product variations to be synced in the background $integration->on_product_publish( $product->get_id() ); } elseif ( $integration->product_should_be_synced( $product ) ) { // schedule simple products to be updated or deleted from the catalog in the background if ( Products::product_should_be_deleted( $product ) ) { facebook_for_woocommerce()->get_products_sync_handler()->delete_products( array( $product->get_id() ) ); } else { facebook_for_woocommerce()->get_products_sync_handler()->create_or_update_products( array( $product->get_id() ) ); } } } } /** * Adds a message for after a product or set of products get excluded from sync. * * @since 2.0.0 */ public function maybe_show_product_disabled_sync_notice() { $transient_name = 'wc_' . facebook_for_woocommerce()->get_id() . '_show_product_disabled_sync_notice_' . get_current_user_id(); $message_id = 'wc-' . facebook_for_woocommerce()->get_id_dasherized() . '-product-disabled-sync'; $count = get_transient( $transient_name ); if ( $count && ( Helper::is_current_screen( 'edit-product' ) || Helper::is_current_screen( 'product' ) ) ) { $message = sprintf( /* translators: Placeholders: %1$s - <strong> tag, %2$s - </strong> tag, %3$s - <a> tag, %4$s - <a> tag */ _n( '%1$sHeads up!%2$s If this product was previously visible in Facebook, you may need to delete it from the %3$sFacebook catalog%4$s to completely hide it from customer view.', '%1$sHeads up!%2$s If these products were previously visible in Facebook, you may need to delete them from the %3$sFacebook catalog%4$s to completely hide them from customer view.', $count, 'facebook-for-woocommerce' ), '<strong>', '</strong>', '<a href="https://facebook.com/products" target="_blank">', '</a>' ); $message .= '<a class="button js-wc-plugin-framework-notice-dismiss">' . esc_html__( "Don't show this notice again", 'facebook-for-woocommerce' ) . '</a>'; facebook_for_woocommerce()->get_admin_notice_handler()->add_admin_notice( $message, $message_id, array( 'dismissible' => false, // we add our own dismiss button 'notice_class' => 'notice-info', ) ); delete_transient( $transient_name ); }//end if } /** * Prints a notice on products page to inform users that the virtual products selected for the Include bulk action will have sync enabled, but will be hidden. * * @internal * * @since 1.11.3-dev.2 */ public function maybe_add_enabling_virtual_products_sync_notice() { $show_notice_transient_name = 'wc_' . facebook_for_woocommerce()->get_id() . '_enabling_virtual_products_sync_show_notice_' . get_current_user_id(); $affected_products_transient_name = 'wc_' . facebook_for_woocommerce()->get_id() . '_enabling_virtual_products_sync_affected_products_' . get_current_user_id(); $affected_products = get_transient( $affected_products_transient_name ); if ( Helper::is_current_screen( 'edit-product' ) && get_transient( $show_notice_transient_name ) && $affected_products ) { $message = sprintf( esc_html( /* translators: Placeholders: %1$s - number of affected products, %2$s opening HTML <a> tag, %3$s - closing HTML </a> tag, %4$s - opening HTML <a> tag, %5$s - closing HTML </a> tag */ _n( '%2$s%1$s product%3$s or some of its variations could not be updated to show in the Facebook catalog — %4$sFacebook Commerce Policies%5$s prohibit selling some product types (like virtual products). You may still advertise Virtual products on Facebook.', '%2$s%1$s products%3$s or some of their variations could not be updated to show in the Facebook catalog — %4$sFacebook Commerce Policies%5$s prohibit selling some product types (like virtual products). You may still advertise Virtual products on Facebook.', count( $affected_products ), 'facebook-for-woocommerce' ) ), count( $affected_products ), '<a href="' . esc_url( add_query_arg( array( 'facebook_show_affected_products' => 1 ) ) ) . '">', '</a>', '<a href="https://www.facebook.com/policies/commerce/prohibited_content/subscriptions_and_digital_products" target="_blank">', '</a>' ); facebook_for_woocommerce()->get_admin_notice_handler()->add_admin_notice( $message, 'wc-' . facebook_for_woocommerce()->get_id_dasherized() . '-enabling-virtual-products-sync', array( 'dismissible' => false, 'notice_class' => 'notice-info', ) ); delete_transient( $show_notice_transient_name ); }//end if } /** * Tweaks the query to show a filtered view with the affected products. * * @internal * * @since 2.0.0 * * @param array $query_vars product query vars for the edit screen * @return array */ public function filter_virtual_products_affected_enabling_sync( $query_vars ) { $transient_name = 'wc_' . facebook_for_woocommerce()->get_id() . '_enabling_virtual_products_sync_affected_products_' . get_current_user_id(); $affected_products = get_transient( $transient_name ); // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['facebook_show_affected_products'] ) && Helper::is_current_screen( 'edit-product' ) && $affected_products ) { $query_vars['post__in'] = $affected_products; } // phpcs:enable WordPress.Security.NonceVerification.Recommended return $query_vars; } /** * Prints a notice to inform sync mode has been automatically set to Sync and hide for virtual products and variations. * * @internal * * @since 2.0.0 */ public function add_handled_virtual_products_variations_notice() { if ( 'yes' === get_option( 'wc_facebook_background_handle_virtual_products_variations_complete', 'no' ) && 'yes' !== get_option( 'wc_facebook_background_handle_virtual_products_variations_skipped', 'no' ) ) { facebook_for_woocommerce()->get_admin_notice_handler()->add_admin_notice( sprintf( /* translators: Placeholders: %1$s - opening HTML <strong> tag, %2$s - closing HTML </strong> tag, %3$s - opening HTML <a> tag, %4$s - closing HTML </a> tag */ esc_html__( '%1$sHeads up!%2$s Facebook\'s %3$sCommerce Policies%4$s do not support selling virtual products, so we have hidden your synced Virtual products in your Facebook catalog. You may still advertise Virtual products on Facebook.', 'facebook-for-woocommerce' ), '<strong>', '</strong>', '<a href="https://www.facebook.com/policies/commerce/prohibited_content/subscriptions_and_digital_products" target="_blank">', '</a>' ), 'wc-' . facebook_for_woocommerce()->get_id_dasherized() . '-updated-virtual-products-sync', array( 'notice_class' => 'notice-info', 'always_show_on_settings' => false, ) ); } } /** * Adds a new tab to the Product edit page. * * @internal * * @since 1.10.0 * * @param array $tabs product tabs * @return array */ public function add_product_settings_tab( $tabs ) { $tabs['fb_commerce_tab'] = array( 'label' => __( 'Facebook', 'facebook-for-woocommerce' ), 'target' => 'facebook_options', 'class' => array( 'show_if_simple', 'show_if_variable', 'show_if_external' ), ); return $tabs; } /** * Outputs the form field for Facebook Product Videos with a description tip. * * @param array $video_urls Array of video URLs. */ private function render_facebook_product_video_field( $video_urls ) { $attachment_ids = []; // Output the form field for Facebook Product Videos with a description tip ?> <p class="form-field fb_product_video_field"> <label for="fb_product_video"><?php esc_html_e( 'Facebook Product Video', 'facebook-for-woocommerce' ); ?></label> <button type="button" class="button" id="open_media_library" name="fb_product_video"><?php esc_html_e( 'Choose', 'facebook-for-woocommerce' ); ?></button> <span class="woocommerce-help-tip" data-tip="<?php esc_attr_e( 'Choose the product video that should be synced to the Facebook catalog and displayed for this product.', 'facebook-for-woocommerce' ); ?>" tabindex="0"></span> </p> <div id="fb_product_video_selected_thumbnails"> <?php if ( ! empty( $video_urls ) ) { foreach ( $video_urls as $video_url ) { $attachment_id = attachment_url_to_postid( $video_url ); if ( $attachment_id ) { $attachment_ids[] = $attachment_id; // Get the video thumbnail URL $thumbnail_url = wp_get_attachment_image_url( $attachment_id, 'thumbnail' ); if ( ! $thumbnail_url ) { // Fallback to a default icon if no thumbnail is available $thumbnail_url = esc_url( wp_mime_type_icon( 'video' ) ); } // Escape URLs and attributes $video_url_escaped = esc_url( $video_url ); $attachment_id_escaped = esc_attr( $attachment_id ); ?> <p class="form-field video-thumbnail"> <img src="<?php echo esc_url( $thumbnail_url ); ?>"> <span data-attachment-id="<?php echo esc_attr( $attachment_id_escaped ); ?>"><?php echo esc_html( $video_url_escaped ); ?></span> <a href="#" class="remove-video" data-attachment-id="<?php echo esc_attr( $attachment_id_escaped ); ?>"><?php esc_html_e( 'Remove', 'facebook-for-woocommerce' ); ?></a> </p> <?php } } } ?> </div> <?php // hidden input to store attachment IDs woocommerce_wp_hidden_input( [ 'id' => \WC_Facebook_Product::FB_PRODUCT_VIDEO, 'name' => \WC_Facebook_Product::FB_PRODUCT_VIDEO, 'value' => esc_attr( implode( ',', $attachment_ids ) ), // Store attachment IDs ] ); } /** * Renders the Facebook Product Images field for variations. * * @param array $attachment_ids Array of attachment IDs. * @param int $index The variation index. * @param int $variation_id The variation ID. */ private function render_facebook_product_images_field( $attachment_ids, $index, $variation_id ) { // Check if multiple images feature is enabled via rollout switch $plugin = isset( $GLOBALS['wc_facebook_commerce'] ) ? $GLOBALS['wc_facebook_commerce'] : facebook_for_woocommerce(); if ( ! $plugin || ! $plugin->get_rollout_switches()->is_switch_enabled( RolloutSwitches::SWITCH_MULTIPLE_IMAGES_ENABLED ) ) { return; } // attachment_ids is already an array of attachment IDs // Output the form field for Facebook Product Images with a description tip ?> <p class="form-field product-image-source-field show-if-product-image-source-<?php echo esc_attr( Products::PRODUCT_IMAGE_SOURCE_MULTIPLE ); ?>"> <!-- <label for="fb_product_images_<?php echo esc_attr( $index ); ?>"><?php esc_html_e( 'Facebook Product Images', 'facebook-for-woocommerce' ); ?></label> --> <button type="button" class="button fb-open-images-library" data-variation-index="<?php echo esc_attr( $index ); ?>" data-variation-id="<?php echo esc_attr( $variation_id ); ?>"><?php esc_html_e( 'Add Multiple Images', 'facebook-for-woocommerce' ); ?></button> <span class="woocommerce-help-tip" data-tip="<?php esc_attr_e( 'Choose multiple product images that should be synced to the Facebook catalog and displayed for this variation.', 'facebook-for-woocommerce' ); ?>" tabindex="0"></span> <div id="fb_product_images_selected_thumbnails_<?php echo esc_attr( $index ); ?>" class="fb-product-images-thumbnails"> <?php if ( ! empty( $attachment_ids ) && is_array( $attachment_ids ) ) { foreach ( $attachment_ids as $attachment_id ) { $attachment_id = intval( $attachment_id ); if ( $attachment_id > 0 ) { // Get the image thumbnail URL $thumbnail_url = wp_get_attachment_image_url( $attachment_id, 'thumbnail' ); $full_url = wp_get_attachment_url( $attachment_id ); $filename = basename( get_attached_file( $attachment_id ) ); if ( $thumbnail_url && $full_url ) { ?> <p class="form-field image-thumbnail"> <img src="<?php echo esc_url( $thumbnail_url ); ?>"> <span data-attachment-id="<?php echo esc_attr( $attachment_id ); ?>"><?php echo esc_html( $filename ); ?></span> <a href="#" class="remove-image" data-attachment-id="<?php echo esc_attr( $attachment_id ); ?>"><?php esc_html_e( 'Remove', 'facebook-for-woocommerce' ); ?></a> </p> <?php } } } } ?> </div> <?php // hidden input to store attachment IDs woocommerce_wp_hidden_input( [ 'id' => sprintf( 'variable_%s%s', \WC_Facebook_Product::FB_PRODUCT_IMAGES, $index ), 'name' => sprintf( 'variable_%s%s', \WC_Facebook_Product::FB_PRODUCT_IMAGES, $index ), 'value' => esc_attr( implode( ',', $attachment_ids ) ), // Store attachment IDs ] ); ?> </p> <?php } /** * Adds content to the new Facebook tab on the Product edit page. * * @internal * * @since 1.10.0 */ public function add_product_settings_tab_content() { global $post; // all products have sync enabled unless explicitly disabled $sync_enabled = 'no' !== get_post_meta( $post->ID, Products::get_product_sync_meta_key(), true ); $visibility = get_post_meta( $post->ID, Products::VISIBILITY_META_KEY, true ); $is_visible = $visibility ? wc_string_to_bool( $visibility ) : true; $product = wc_get_product( $post ); $rich_text_description = get_post_meta( $post->ID, \WC_Facebookcommerce_Integration::FB_RICH_TEXT_DESCRIPTION, true ); $price = get_post_meta( $post->ID, \WC_Facebook_Product::FB_PRODUCT_PRICE, true ); $image_source = get_post_meta( $post->ID, Products::PRODUCT_IMAGE_SOURCE_META_KEY, true ); $image = get_post_meta( $post->ID, \WC_Facebook_Product::FB_PRODUCT_IMAGE, true ); $video_urls = get_post_meta( $post->ID, \WC_Facebook_Product::FB_PRODUCT_VIDEO, true ); $fb_brand = get_post_meta( $post->ID, \WC_Facebook_Product::FB_BRAND, true ) ? get_post_meta( $post->ID, \WC_Facebook_Product::FB_BRAND, true ) : get_post_meta( $post->ID, '_wc_facebook_enhanced_catalog_attributes_brand', true ); $fb_mpn = get_post_meta( $post->ID, \WC_Facebook_Product::FB_MPN, true ); $fb_condition = get_post_meta( $post->ID, \WC_Facebook_Product::FB_PRODUCT_CONDITION, true ); $fb_age_group = get_post_meta( $post->ID, \WC_Facebook_Product::FB_AGE_GROUP, true ) ? get_post_meta( $post->ID, \WC_Facebook_Product::FB_AGE_GROUP, true ) : get_post_meta( $post->ID, '_wc_facebook_enhanced_catalog_attributes_age_group', true ); $fb_gender = get_post_meta( $post->ID, \WC_Facebook_Product::FB_GENDER, true ) ? get_post_meta( $post->ID, \WC_Facebook_Product::FB_GENDER, true ) : get_post_meta( $post->ID, '_wc_facebook_enhanced_catalog_attributes_gender', true ); $fb_size = get_post_meta( $post->ID, \WC_Facebook_Product::FB_SIZE, true ) ? get_post_meta( $post->ID, \WC_Facebook_Product::FB_SIZE, true ) : get_post_meta( $post->ID, '_wc_facebook_enhanced_catalog_attributes_size', true ); $fb_color = get_post_meta( $post->ID, \WC_Facebook_Product::FB_COLOR, true ) ? get_post_meta( $post->ID, \WC_Facebook_Product::FB_COLOR, true ) : get_post_meta( $post->ID, '_wc_facebook_enhanced_catalog_attributes_color', true ); $fb_material = get_post_meta( $post->ID, \WC_Facebook_Product::FB_MATERIAL, true ) ? get_post_meta( $post->ID, \WC_Facebook_Product::FB_MATERIAL, true ) : get_post_meta( $post->ID, '_wc_facebook_enhanced_catalog_attributes_material', true ); $fb_pattern = get_post_meta( $post->ID, \WC_Facebook_Product::FB_PATTERN, true ) ? get_post_meta( $post->ID, \WC_Facebook_Product::FB_PATTERN, true ) : get_post_meta( $post->ID, '_wc_facebook_enhanced_catalog_attributes_pattern', true ); if ( $sync_enabled ) { $sync_mode = $is_visible ? self::SYNC_MODE_SYNC_AND_SHOW : self::SYNC_MODE_SYNC_AND_HIDE; } else { $sync_mode = self::SYNC_MODE_SYNC_DISABLED; } // 'id' attribute needs to match the 'target' parameter set above ?> <div id='facebook_options' class='panel woocommerce_options_panel'> <div> <?php woocommerce_wp_select( array( 'id' => 'wc_facebook_sync_mode', 'label' => __( 'Facebook Sync', 'facebook-for-woocommerce' ), 'options' => array( self::SYNC_MODE_SYNC_AND_SHOW => __( 'Sync and show in catalog', 'facebook-for-woocommerce' ), self::SYNC_MODE_SYNC_AND_HIDE => __( 'Sync and hide in catalog', 'facebook-for-woocommerce' ), self::SYNC_MODE_SYNC_DISABLED => __( 'Do not sync', 'facebook-for-woocommerce' ), ), 'value' => $sync_mode, 'desc_tip' => true, 'description' => __( 'Choose whether to sync this product to Facebook and, if synced, whether it should be visible in the catalog.', 'facebook-for-woocommerce' ), ) ); ?> </div> <?php if ( $product && $product->is_type( 'variable' ) ) { // Render video field only for variable products $this->render_facebook_product_video_field( $video_urls ); } ?> <div class='options_group hide_if_variable'> <?php echo '<div class="wp-editor-wrap">'; echo '<label for="' . esc_attr( \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION ) . '">' . esc_html__( 'Facebook Description', 'facebook-for-woocommerce' ) . '</label>'; wp_editor( $rich_text_description, \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION, array( 'id' => \WC_Facebook_Product::FB_PRODUCT_DESCRIPTION, 'textarea_name' => \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION, 'textarea_rows' => 10, 'media_buttons' => true, 'teeny' => true, 'quicktags' => false, 'tinymce' => array( 'toolbar1' => 'bold,italic,bullist,spellchecker,fullscreen', ), ) ); echo '</div>'; woocommerce_wp_radio( array( 'id' => 'fb_product_image_source', 'label' => __( 'Facebook Product Image', 'facebook-for-woocommerce' ), 'desc_tip' => true, 'description' => __( 'Choose the product image that should be synced to the Facebook catalog and displayed for this product.', 'facebook-for-woocommerce' ), 'options' => array( Products::PRODUCT_IMAGE_SOURCE_PRODUCT => __( 'Use WooCommerce image', 'facebook-for-woocommerce' ), Products::PRODUCT_IMAGE_SOURCE_CUSTOM => __( 'Use custom image', 'facebook-for-woocommerce' ), ), 'value' => $image_source ? $image_source : Products::PRODUCT_IMAGE_SOURCE_PRODUCT, 'class' => 'short enable-if-sync-enabled js-fb-product-image-source', 'wrapper_class' => 'fb-product-image-source-field', ) ); woocommerce_wp_text_input( array( 'id' => \WC_Facebook_Product::FB_PRODUCT_IMAGE, 'label' => __( 'Custom Image URL', 'facebook-for-woocommerce' ), 'value' => $image, 'class' => sprintf( 'enable-if-sync-enabled product-image-source-field show-if-product-image-source-%s', Products::PRODUCT_IMAGE_SOURCE_CUSTOM ), 'desc_tip' => true, 'description' => __( 'Please enter an absolute URL (e.g. https://domain.com/image.jpg).', 'facebook-for-woocommerce' ), ) ); // Render the Facebook Product Video field at Product level $this->render_facebook_product_video_field( $video_urls ); woocommerce_wp_text_input( array( 'id' => \WC_Facebook_Product::FB_PRODUCT_PRICE, 'label' => sprintf( /* translators: Placeholders %1$s - WC currency symbol */ __( 'Facebook Price (%1$s)', 'facebook-for-woocommerce' ), get_woocommerce_currency_symbol() ), 'desc_tip' => true, 'description' => __( 'Custom price for product on Facebook. Please enter in monetary decimal (.) format without thousand separators and currency symbols. If blank, product price will be used.', 'facebook-for-woocommerce' ), 'cols' => 40, 'rows' => 60, 'value' => $price, 'class' => 'enable-if-sync-enabled', ) ); woocommerce_wp_hidden_input( array( 'id' => \WC_Facebook_Product::FB_REMOVE_FROM_SYNC, 'value' => '', ) ); ?> </div> <div class='wc_facebook_commerce_fields'> <p class="text-heading"> <span><?php echo esc_html( \WooCommerce\Facebook\Admin\Product_Categories::get_catalog_explanation_text() ); ?></span> <a href="#" class="go-to-attributes-link" style="text-decoration: underline; cursor: pointer;"> <?php echo esc_html__( 'Go to attributes', 'facebook-for-woocommerce' ); ?> </a> </p> </div> <script type="text/javascript"> jQuery(document).ready(function($) { $('.go-to-attributes-link').click(function(e) { e.preventDefault(); $('li.attribute_options.attribute_tab a[href="#product_attributes"]').trigger('click'); $('html, body').animate({ scrollTop: $('#product_attributes').offset().top - 50 }, 500); }); }); </script> <?php woocommerce_wp_text_input( array( 'id' => \WC_Facebook_Product::FB_MPN, 'name' => \WC_Facebook_Product::FB_MPN, 'label' => __( 'Manufacturer Part Number (MPN)', 'facebook-for-woocommerce' ), 'value' => $fb_mpn, 'class' => 'enable-if-sync-enabled', 'desc_tip' => true, 'description' => __( 'Manufacturer Part Number (MPN) of the item', 'facebook-for-woocommerce' ), ) ); woocommerce_wp_text_input( array( 'id' => \WC_Facebook_Product::FB_BRAND, 'name' => \WC_Facebook_Product::FB_BRAND, 'label' => __( 'Brand', 'facebook-for-woocommerce' ), 'value' => $fb_brand, 'class' => 'enable-if-sync-enabled', 'desc_tip' => true, 'description' => __( 'Brand name of the item', 'facebook-for-woocommerce' ), 'placeholder' => \WC_Facebookcommerce_Utils::get_default_fb_brand(), ) ); woocommerce_wp_select( array( 'id' => \WC_Facebook_Product::FB_PRODUCT_CONDITION, 'name' => \WC_Facebook_Product::FB_PRODUCT_CONDITION, 'label' => __( 'Condition', 'facebook-for-woocommerce' ), 'options' => array( '' => __( 'Select', 'facebook-for-woocommerce' ), \WC_Facebook_Product::CONDITION_NEW => __( 'New', 'facebook-for-woocommerce' ), \WC_Facebook_Product::CONDITION_REFURBISHED => __( 'Refurbished', 'facebook-for-woocommerce' ), \WC_Facebook_Product::CONDITION_USED => __( 'Used', 'facebook-for-woocommerce' ), ), 'value' => $fb_condition, 'desc_tip' => true, 'description' => __( 'This refers to the condition of your product. Supported values are new, refurbished and used.', 'facebook-for-woocommerce' ), ) ); woocommerce_wp_text_input( array( 'id' => \WC_Facebook_Product::FB_SIZE, 'label' => __( 'Size', 'facebook-for-woocommerce' ), 'desc_tip' => true, 'description' => __( 'Size of the product item', 'facebook-for-woocommerce' ), 'name' => \WC_Facebook_Product::FB_SIZE, 'cols' => 40, 'rows' => 60, 'value' => $fb_size, 'class' => 'enable-if-sync-enabled', ) ); woocommerce_wp_text_input( array( 'id' => \WC_Facebook_Product::FB_COLOR, 'name' => \WC_Facebook_Product::FB_COLOR, 'label' => __( 'Color', 'facebook-for-woocommerce' ), 'desc_tip' => true, 'description' => __( 'Color of the product item', 'facebook-for-woocommerce' ), 'cols' => 40, 'rows' => 60, 'value' => $fb_color, 'class' => 'enable-if-sync-enabled', ) ); woocommerce_wp_select( array( 'id' => \WC_Facebook_Product::FB_AGE_GROUP, 'name' => \WC_Facebook_Product::FB_AGE_GROUP, 'label' => __( 'Age Group', 'facebook-for-woocommerce' ), 'options' => array( '' => __( 'Select', 'facebook-for-woocommerce' ), \WC_Facebook_Product::AGE_GROUP_ADULT => __( 'Adult', 'facebook-for-woocommerce' ), \WC_Facebook_Product::AGE_GROUP_ALL_AGES => __( 'All Ages', 'facebook-for-woocommerce' ), \WC_Facebook_Product::AGE_GROUP_TEEN => __( 'Teen', 'facebook-for-woocommerce' ), \WC_Facebook_Product::AGE_GROUP_KIDS => __( 'Kids', 'facebook-for-woocommerce' ), \WC_Facebook_Product::AGE_GROUP_TODDLER => __( 'Toddler', 'facebook-for-woocommerce' ), \WC_Facebook_Product::AGE_GROUP_INFANT => __( 'Infant', 'facebook-for-woocommerce' ), \WC_Facebook_Product::AGE_GROUP_NEWBORN => __( 'Newborn', 'facebook-for-woocommerce' ), ), 'value' => $fb_age_group, 'desc_tip' => true, 'description' => __( 'Select the age group for this product.', 'facebook-for-woocommerce' ), ) ); woocommerce_wp_select( array( 'id' => \WC_Facebook_Product::FB_GENDER, 'name' => \WC_Facebook_Product::FB_GENDER, 'label' => __( 'Gender', 'facebook-for-woocommerce' ), 'options' => array( '' => __( 'Select', 'facebook-for-woocommerce' ), \WC_Facebook_Product::GENDER_FEMALE => __( 'Female', 'facebook-for-woocommerce' ), \WC_Facebook_Product::GENDER_MALE => __( 'Male', 'facebook-for-woocommerce' ), \WC_Facebook_Product::GENDER_UNISEX => __( 'Unisex', 'facebook-for-woocommerce' ), ), 'value' => $fb_gender, 'desc_tip' => true, 'description' => __( 'Select the gender for this product.', 'facebook-for-woocommerce' ), ) ); woocommerce_wp_text_input( array( 'id' => \WC_Facebook_Product::FB_MATERIAL, 'label' => __( 'Material', 'facebook-for-woocommerce' ), 'desc_tip' => true, 'description' => __( 'Material of the product item', 'facebook-for-woocommerce' ), 'name' => \WC_Facebook_Product::FB_MATERIAL, 'cols' => 40, 'rows' => 60, 'value' => $fb_material, 'class' => 'enable-if-sync-enabled', ) ); woocommerce_wp_text_input( array( 'id' => \WC_Facebook_Product::FB_PATTERN, 'label' => __( 'Pattern', 'facebook-for-woocommerce' ), 'desc_tip' => true, 'description' => __( 'Pattern of the product item', 'facebook-for-woocommerce' ), 'name' => \WC_Facebook_Product::FB_PATTERN, 'cols' => 40, 'rows' => 60, 'value' => $fb_pattern, 'class' => 'enable-if-sync-enabled', ) ); ?> <div class='wc-facebook-commerce-options-group options_group google_product_catgory'> <?php \WooCommerce\Facebook\Admin\Products::render_google_product_category_fields_and_enhanced_attributes( $product ); ?> </div> </div> <?php } /** * Outputs the Facebook settings fields for a single variation. * * @internal * * @since 1.10.0 * * @param int $index the index of the current variation * @param array $variation_data unused * @param \WC_Post $post the post type for the current variation */ public function add_product_variation_edit_fields( $index, $variation_data, $post ) { $variation = wc_get_product( $post ); if ( ! $variation instanceof \WC_Product_Variation ) { return; } $parent = wc_get_product( $variation->get_parent_id() ); if ( ! $parent instanceof \WC_Product ) { return; } // Get variation meta values $description = $this->get_product_variation_meta( $variation, \WC_Facebookcommerce_Integration::FB_RICH_TEXT_DESCRIPTION, $parent ); $price = $this->get_product_variation_meta( $variation, \WC_Facebook_Product::FB_PRODUCT_PRICE, $parent ); $image_url = $this->get_product_variation_meta( $variation, \WC_Facebook_Product::FB_PRODUCT_IMAGE, $parent ); $image_source = $variation->get_meta( Products::PRODUCT_IMAGE_SOURCE_META_KEY ); $image_urls = $this->get_product_variation_meta( $variation, \WC_Facebook_Product::FB_PRODUCT_IMAGES, $parent ); $fb_mpn = $this->get_product_variation_meta( $variation, \WC_Facebook_Product::FB_MPN, $parent ); ?> <div class="facebook-metabox wc-metabox closed"> <h3> <strong><?php esc_html_e( 'Facebook for WooCommerce', 'facebook-for-woocommerce' ); ?></strong> <div class="handlediv" aria-label="<?php esc_attr_e( 'Click to toggle', 'facebook-for-woocommerce' ); ?>"></div> </h3> <div class="wc-metabox-content" style="display: none;"> <?php wp_nonce_field( 'facebook_variation_save', 'facebook_variation_nonce_' . $variation->get_id() ); ?> <?php woocommerce_wp_textarea_input( array( 'id' => sprintf( 'variable_%s%s', \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION, $index ), 'name' => sprintf( 'variable_%s[%s]', \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION, $index ), 'label' => __( 'Facebook Description', 'facebook-for-woocommerce' ), 'desc_tip' => true, 'description' => __( 'Custom (plain-text only) description for product on Facebook. If blank, product description will be used. If product description is blank, shortname will be used.', 'facebook-for-woocommerce' ), 'value' => $description, 'class' => 'enable-if-sync-enabled', 'wrapper_class' => 'form-row form-row-full', ) ); // Build image source options $image_source_options = array( Products::PRODUCT_IMAGE_SOURCE_PRODUCT => __( 'Use variation image', 'facebook-for-woocommerce' ), Products::PRODUCT_IMAGE_SOURCE_PARENT_PRODUCT => __( 'Use parent image', 'facebook-for-woocommerce' ), Products::PRODUCT_IMAGE_SOURCE_CUSTOM => __( 'Use custom image', 'facebook-for-woocommerce' ), ); // Add multiple images option only if rollout switch is enabled $plugin = isset( $GLOBALS['wc_facebook_commerce'] ) ? $GLOBALS['wc_facebook_commerce'] : facebook_for_woocommerce(); if ( $plugin && $plugin->get_rollout_switches()->is_switch_enabled( RolloutSwitches::SWITCH_MULTIPLE_IMAGES_ENABLED ) ) { $image_source_options[ Products::PRODUCT_IMAGE_SOURCE_MULTIPLE ] = __( 'Add multiple images', 'facebook-for-woocommerce' ); } woocommerce_wp_radio( array( 'id' => "variable_fb_product_image_source$index", 'name' => "variable_fb_product_image_source[$index]", 'label' => __( 'Facebook Product Image', 'facebook-for-woocommerce' ), 'desc_tip' => true, 'description' => __( 'Choose the product image that should be synced to the Facebook catalog and displayed for this product.', 'facebook-for-woocommerce' ), 'options' => $image_source_options, 'value' => $image_source ? $image_source : Products::PRODUCT_IMAGE_SOURCE_PRODUCT, 'class' => 'enable-if-sync-enabled js-fb-product-image-source', 'wrapper_class' => 'fb-product-image-source-field', ) ); woocommerce_wp_text_input( array( 'id' => sprintf( 'variable_%s%s', \WC_Facebook_Product::FB_PRODUCT_IMAGE, $index ), 'name' => sprintf( 'variable_%s[%s]', \WC_Facebook_Product::FB_PRODUCT_IMAGE, $index ), 'label' => __( 'Custom Image URL', 'facebook-for-woocommerce' ), 'value' => $image_url, 'class' => sprintf( 'enable-if-sync-enabled product-image-source-field show-if-product-image-source-%s', Products::PRODUCT_IMAGE_SOURCE_CUSTOM ), 'wrapper_class' => 'form-row form-row-full', 'desc_tip' => true, 'description' => __( 'Please enter an absolute URL (e.g. https://domain.com/image.jpg).', 'facebook-for-woocommerce' ), ) ); // Render Facebook Product Images field $image_ids_array = ! empty( $image_urls ) ? explode( ',', $image_urls ) : []; // Clean up the IDs and ensure they're numeric $image_ids_array = array_filter( array_map( 'trim', $image_ids_array ), 'is_numeric' ); $this->render_facebook_product_images_field( $image_ids_array, $index, $variation->get_id() ); woocommerce_wp_text_input( array( 'id' => sprintf( 'variable_%s%s', \WC_Facebook_Product::FB_PRODUCT_PRICE, $index ), 'name' => sprintf( 'variable_%s[%s]', \WC_Facebook_Product::FB_PRODUCT_PRICE, $index ), 'label' => sprintf( /* translators: Placeholders %1$s - WC currency symbol */ __( 'Facebook Price (%1$s)', 'facebook-for-woocommerce' ), get_woocommerce_currency_symbol() ), 'desc_tip' => true, 'description' => __( 'Custom price for product on Facebook. Please enter in monetary decimal (.) format without thousand separators and currency symbols. If blank, product price will be used.', 'facebook-for-woocommerce' ), 'value' => wc_format_decimal( $price ), 'class' => 'enable-if-sync-enabled', 'wrapper_class' => 'form-row form-full', ) ); woocommerce_wp_text_input( array( 'id' => sprintf( 'variable_%s%s', \WC_Facebook_Product::FB_MPN, $index ), 'name' => sprintf( 'variable_%s[%s]', \WC_Facebook_Product::FB_MPN, $index ), 'label' => __( 'Manufacturer Parts Number (MPN)', 'facebook-for-woocommerce' ), 'desc_tip' => true, 'description' => __( 'Manufacturer Parts Number', 'facebook-for-woocommerce' ), 'value' => $fb_mpn, 'class' => 'enable-if-sync-enabled', 'wrapper_class' => 'form-row form-full', ) ); ?> </div> </div> <script type="text/javascript"> jQuery(document).ready(function($) { // Remove any existing click handlers first $('.facebook-metabox h3, .facebook-metabox .handlediv').off('click'); // Add new click handler $('.facebook-metabox h3, .facebook-metabox .handlediv').on('click', function(e) { e.preventDefault(); // Prevent any default behavior e.stopPropagation(); // Stop event bubbling var $metabox = $(this).closest('.facebook-metabox'); $metabox.toggleClass('closed'); $metabox.find('.wc-metabox-content').slideToggle(); }); // Ensure metaboxes start closed $('.facebook-metabox').addClass('closed') .find('.wc-metabox-content') .hide(); }); </script> <?php } /** * Gets the stored value for the given meta of a product variation. * * If no value is found, we try to use the value stored in the parent product. * * @since 1.10.0 * * @param \WC_Product_Variation $variation the product variation * @param string $key the name of the meta to retrieve * @param \WC_Product $parent_product the parent product * @return mixed */ private function get_product_variation_meta( $variation, $key, $parent_product ) { $value = $variation->get_meta( $key ); if ( '' === $value && $parent_product instanceof \WC_Product ) { $value = $parent_product->get_meta( $key ); } return $value; } /** * Saves the submitted Facebook settings for each variation. * * @internal * * @since 1.10.0 * * @param int $variation_id the ID of the product variation being edited * @param int $index the index of the current variation */ public function save_product_variation_edit_fields( $variation_id, $index ) { $variation = wc_get_product( $variation_id ); if ( ! $variation instanceof \WC_Product_Variation ) { return; } if ( ! $this->verify_variation_nonce( $variation_id ) ) { return; } // phpcs:disable WordPress.Security.NonceVerification.Missing $sync_mode = $this->determine_variation_sync_mode( $variation ); $sync_enabled = self::SYNC_MODE_SYNC_DISABLED !== $sync_mode; $variation_data = $this->process_variation_post_data( $index ); $this->save_variation_meta_data( $variation, $variation_data ); $this->handle_variation_sync_operations( $variation, $sync_enabled, $sync_mode ); // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Verifies the nonce for variation save operation. * * @param int $variation_id the ID of the product variation * @return bool true if nonce is valid, false otherwise */ private function verify_variation_nonce( $variation_id ) { $nonce_field = 'facebook_variation_nonce_' . $variation_id; return isset( $_POST[ $nonce_field ] ) && wp_verify_nonce( sanitize_key( $_POST[ $nonce_field ] ), 'facebook_variation_save' ); } /** * Determines the sync mode for a variation. * * @param \WC_Product_Variation $variation the product variation * @return string the sync mode */ private function determine_variation_sync_mode( $variation ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification is handled in save_product_variation_edit_fields method $sync_mode = isset( $_POST['wc_facebook_sync_mode'] ) ? wc_clean( wp_unslash( $_POST['wc_facebook_sync_mode'] ) ) : self::SYNC_MODE_SYNC_DISABLED; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification is handled in save_product_variation_edit_fields method if ( ! isset( $_POST['wc_facebook_sync_mode'] ) ) { $sync_mode = $this->get_parent_product_sync_mode( $variation ); } if ( self::SYNC_MODE_SYNC_AND_SHOW === $sync_mode && $variation->is_virtual() ) { $sync_mode = self::SYNC_MODE_SYNC_AND_HIDE; } return $sync_mode; } /** * Gets the sync mode from the parent product. * * @param \WC_Product_Variation $variation the product variation * @return string the sync mode */ private function get_parent_product_sync_mode( $variation ) { $parent_product = wc_get_product( $variation->get_parent_id() ); if ( ! $parent_product ) { return self::SYNC_MODE_SYNC_DISABLED; } $parent_sync_enabled = 'no' !== get_post_meta( $parent_product->get_id(), Products::SYNC_ENABLED_META_KEY, true ); $parent_visibility = get_post_meta( $parent_product->get_id(), Products::VISIBILITY_META_KEY, true ); $parent_is_visible = $parent_visibility ? wc_string_to_bool( $parent_visibility ) : true; if ( $parent_sync_enabled ) { return $parent_is_visible ? self::SYNC_MODE_SYNC_AND_SHOW : self::SYNC_MODE_SYNC_AND_HIDE; } return self::SYNC_MODE_SYNC_DISABLED; } /** * Processes and sanitizes POST data for variation. * * @param int $index the variation index * @return array the processed variation data */ private function process_variation_post_data( $index ) { $posted_param = 'variable_' . \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing -- Intentionally getting raw value to apply different sanitization methods below, nonce verification handled in save_product_variation_edit_fields method $description_raw = isset( $_POST[ $posted_param ][ $index ] ) ? wp_unslash( $_POST[ $posted_param ][ $index ] ) : null; // Create separate sanitized versions for different purposes $description_plain = $description_raw ? sanitize_text_field( $description_raw ) : null; // Plain text for regular description $description_rich = $description_raw ? wp_kses_post( $description_raw ) : null; // HTML-preserved for rich text description $posted_param = 'variable_' . \WC_Facebook_Product::FB_MPN; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification is handled in save_product_variation_edit_fields method $fb_mpn = isset( $_POST[ $posted_param ][ $index ] ) ? sanitize_text_field( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) : null; $posted_param = 'variable_fb_product_image_source'; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification is handled in save_product_variation_edit_fields method $image_source = isset( $_POST[ $posted_param ][ $index ] ) ? sanitize_key( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) : ''; $posted_param = 'variable_' . \WC_Facebook_Product::FB_PRODUCT_IMAGE; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification is handled in save_product_variation_edit_fields method $image_url = isset( $_POST[ $posted_param ][ $index ] ) ? esc_url_raw( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) : null; $posted_param = 'variable_' . \WC_Facebook_Product::FB_PRODUCT_VIDEO; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification is handled in save_product_variation_edit_fields method $video_urls = isset( $_POST[ $posted_param ][ $index ] ) ? esc_url_raw( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) : []; // Fix: Look for the actual POST key format that WooCommerce generates $posted_param = 'variable_' . \WC_Facebook_Product::FB_PRODUCT_IMAGES . $index; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification is handled in save_product_variation_edit_fields method $image_ids = isset( $_POST[ $posted_param ] ) ? sanitize_text_field( wp_unslash( $_POST[ $posted_param ] ) ) : ''; $posted_param = 'variable_' . \WC_Facebook_Product::FB_PRODUCT_PRICE; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification is handled in save_product_variation_edit_fields method $price = isset( $_POST[ $posted_param ][ $index ] ) ? wc_format_decimal( wc_clean( wp_unslash( $_POST[ $posted_param ][ $index ] ) ) ) : ''; return array( 'description_plain' => $description_plain, 'description_rich' => $description_rich, 'fb_mpn' => $fb_mpn, 'image_source' => $image_source, 'image_url' => $image_url, 'video_urls' => $video_urls, 'image_ids' => $image_ids, 'price' => $price, ); } /** * Saves the variation meta data. * * @param \WC_Product_Variation $variation the product variation * @param array $data the variation data to save */ private function save_variation_meta_data( $variation, $data ) { $variation->update_meta_data( \WC_Facebookcommerce_Integration::FB_PRODUCT_DESCRIPTION, $data['description_plain'] ); $variation->update_meta_data( \WC_Facebookcommerce_Integration::FB_RICH_TEXT_DESCRIPTION, $data['description_rich'] ); $variation->update_meta_data( Products::PRODUCT_IMAGE_SOURCE_META_KEY, $data['image_source'] ); $variation->update_meta_data( \WC_Facebook_Product::FB_MPN, $data['fb_mpn'] ); $variation->update_meta_data( \WC_Facebook_Product::FB_PRODUCT_IMAGE, $data['image_url'] ); $variation->update_meta_data( \WC_Facebook_Product::FB_PRODUCT_VIDEO, $data['video_urls'] ); $variation->update_meta_data( \WC_Facebook_Product::FB_PRODUCT_IMAGES, $data['image_ids'] ); $variation->update_meta_data( \WC_Facebook_Product::FB_PRODUCT_PRICE, $data['price'] ); $variation->save_meta_data(); } /** * Handles sync operations for the variation. * * @param \WC_Product_Variation $variation the product variation * @param bool $sync_enabled whether sync is enabled * @param string $sync_mode the sync mode */ private function handle_variation_sync_operations( $variation, $sync_enabled, $sync_mode ) { if ( $sync_enabled ) { Products::enable_sync_for_products( array( $variation ) ); Products::set_product_visibility( $variation, self::SYNC_MODE_SYNC_AND_HIDE !== $sync_mode ); } else { Products::disable_sync_for_products( array( $variation ) ); } // phpcs:enable WordPress.Security.NonceVerification.Missing } /** * Outputs a modal template in admin product pages. * * @internal * * @since 1.10.0 */ public function render_modal_template() { global $current_screen; // bail if not on the products, product edit, or settings screen if ( ! $current_screen || ! in_array( $current_screen->id, $this->screen_ids, true ) ) { return; } ?> <script type="text/template" id="tmpl-facebook-for-woocommerce-modal"> <div class="wc-backbone-modal facebook-for-woocommerce-modal"> <div class="wc-backbone-modal-content"> <section class="wc-backbone-modal-main" role="main"> <header class="wc-backbone-modal-header"> <h1><?php esc_html_e( 'Facebook for WooCommerce', 'facebook-for-woocommerce' ); ?></h1> <button class="modal-close modal-close-link dashicons dashicons-no-alt"> <span class="screen-reader-text"><?php esc_html_e( 'Close modal panel', 'facebook-for-woocommerce' ); ?></span> </button> </header> <article>{{{data.message}}}</article> <footer> <div class="inner">{{{data.buttons}}}</div> </footer> </section> </div> </div> <div class="wc-backbone-modal-backdrop modal-close"></div> </script> <?php } public function add_tab_switch_script() { global $post; if ( ! $post || get_post_type( $post ) !== 'product' ) { return; } ?> <script type="text/javascript"> jQuery(document).ready(function($) { // State object to track badge display status var syncedBadgeState = { material: false, color: false, size: false, pattern: false, brand: false, mpn: false, age_group: false, gender: false, condition: false }; // Store manual input values var manualValues = {}; // Track which fields are currently synced var syncedFields = {}; // Helper function to clean up any previous sync UI elements function cleanupSyncedField(fieldId) { var $field = $(fieldId); // First find all multi-value displays and sync indicators in the parent wrapper var $parent = $field.parent(); $parent.find('.multi-value-display').remove(); $parent.find('.sync-indicator').remove(); // Also remove any elements directly after the field $field.next('.multi-value-display').remove(); $field.next('.sync-indicator').remove(); // Double check for elements with specific classes anywhere in the row var $row = $parent.closest('.form-field, .form-row'); if ($row.length) { $row.find('.multi-value-display').remove(); $row.find('.sync-indicator').remove(); } // Show the original field if it was hidden $field.show(); // Reset the field state $field.prop('disabled', false).removeClass('synced-attribute'); } // Function to completely reset a field to its default state function resetFieldToDefault(fieldId) { var $field = $(fieldId); // Skip if field doesn't exist if (!$field.length) { return; } // Clean up UI elements cleanupSyncedField(fieldId); // Reset select fields to first option (usually "Select") if ($field.is('select')) { // Check if the select has options if ($field.find('option').length > 0) { $field.val('').trigger('change'); $field.find('option:first').prop('selected', true); } // Reset select2 if it's initialized if ($field.hasClass('wc-enhanced-select') || $field.hasClass('select2-hidden-accessible')) { try { $field.select2('val', ''); // Also reset the select2 container styles $field.next('.select2-container').find('.select2-selection').css({ 'cursor': '', 'background-color': '', 'color': '' }); } catch (e) { // Ignore select2 errors } } } // Reset all styles and classes $field .val('') .prop('disabled', false) .removeClass('synced-attribute') .css({ 'cursor': '', 'background-color': '', 'color': '', 'border-color': '', 'opacity': '' }) .show(); // Also reset any select2 container if it exists if ($field.next('.select2-container').length) { $field.next('.select2-container').css({ 'cursor': '', 'opacity': '' }); } } // Function to sync Facebook attributes function syncFacebookAttributes() { // First clean up any stray elements that might exist globally $('.multi-value-display + .wc-attributes-icon').remove(); $('.woocommerce_options_panel').find('.multi-value-display, .wc-attributes-icon').each(function() { // Only remove elements that are duplicates (more than one per field) var $parent = $(this).parent(); var $siblings = $parent.find('.' + $(this).attr('class')); if ($siblings.length > 1) { // Keep only the first one, remove others $siblings.not(':first').remove(); } }); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'sync_facebook_attributes', product_id: <?php echo esc_js( $post->ID ); ?>, nonce: '<?php echo esc_js( wp_create_nonce( 'sync_facebook_attributes' ) ); ?>' }, success: function(response) { if (response.success) { var fields = { 'material': '<?php echo esc_js( \WC_Facebook_Product::FB_MATERIAL ); ?>', 'color': '<?php echo esc_js( \WC_Facebook_Product::FB_COLOR ); ?>', 'size': '<?php echo esc_js( \WC_Facebook_Product::FB_SIZE ); ?>', 'pattern': '<?php echo esc_js( \WC_Facebook_Product::FB_PATTERN ); ?>', 'brand': '<?php echo esc_js( \WC_Facebook_Product::FB_BRAND ); ?>', 'mpn': '<?php echo esc_js( \WC_Facebook_Product::FB_MPN ); ?>', 'age_group': '<?php echo esc_js( \WC_Facebook_Product::FB_AGE_GROUP ); ?>', 'gender': '<?php echo esc_js( \WC_Facebook_Product::FB_GENDER ); ?>', 'condition': '<?php echo esc_js( \WC_Facebook_Product::FB_PRODUCT_CONDITION ); ?>' }; // Loop through each field Object.keys(fields).forEach(function(key) { var fieldId = '#' + fields[key]; var $field = $(fieldId); // Skip if field doesn't exist if (!$field.length) { return; } // First thoroughly clean up any previous sync UI elements cleanupSyncedField(fieldId); if (response.data && response.data[key]) { // Field has a synced value var syncedValue = response.data[key]; var isMultipleValues = syncedValue.includes(' | '); // For fields with multiple values or dropdown fields that need special handling if (isMultipleValues || (key === 'age_group' || key === 'gender' || key === 'condition')) { // First check if this is a standard dropdown or a multi-value field if (isMultipleValues && ($field.is('select') || key === 'age_group' || key === 'gender' || key === 'condition')) { // Check if we already have a multi-value display for this field if ($field.next('.multi-value-display').length === 0) { // For dropdown fields with multiple values (used in variations) // Disable the original dropdown $field.prop('disabled', true).addClass('synced-attribute').hide(); // Create a styled disabled field to show multiple values var fieldWidth = $field.outerWidth(); var $multiDisplay = $('<input type="text" class="multi-value-display wc-enhanced-select" disabled>') .val(syncedValue) .css({ 'width': '50%', 'max-width': '100%', 'height': '34px', 'margin': '0', 'padding': '0 8px', 'background-color': '#f0f0f1', 'border': '1px solid #ddd', 'border-radius': '4px', 'box-sizing': 'border-box', 'font-size': '14px', 'line-height': '32px', 'color': 'rgba(44, 51, 56, .5)', 'display': 'inline-block', 'vertical-align': 'middle', 'cursor': 'not-allowed' }) .insertAfter($field); // Always add the sync badge after the multi-value display // Only if it doesn't already exist if ($multiDisplay.next('.wc-attributes-icon').length === 0) { $multiDisplay.after('<span class="wc-attributes-icon" data-tip="Synced from product attributes"></span>'); } } else { // Update the existing multi-value display $field.next('.multi-value-display').val(syncedValue); } // If this is a dropdown field like gender, age_group, or condition with a single mapped value, // select that value in the dropdown and disable it } else if (key === 'age_group' || key === 'gender' || key === 'condition') { // First check if this dropdown has a corresponding value var hasMatchingOption = false; var matchingOptionValue = ''; $field.find('option').each(function() { var optionValue = $(this).val(); var optionText = $(this).text(); // Check both option value and option text (case-insensitive) if (optionValue === syncedValue || optionText.toLowerCase() === syncedValue.toLowerCase() || optionValue.toLowerCase() === syncedValue.toLowerCase()) { hasMatchingOption = true; matchingOptionValue = optionValue; // Use the actual option value, not the synced value return false; // break loop } }); if (hasMatchingOption) { $field.val(matchingOptionValue) // Use the correct option value .prop('disabled', true) .addClass('synced-attribute') .css({ 'cursor': 'not-allowed', 'background-color': '#f0f0f1', 'color': 'rgba(44, 51, 56, .5)' }); // Add the sync badge if it doesn't exist if ($field.next('.wc-attributes-icon').length === 0) { $field.after('<span class="sync-indicator wc-attributes-icon" data-tip="Synced from the Attributes tab." style="margin-left: 4px;"><span class="sync-tooltip">Synced from the Attributes tab.</span></span>'); } } else if (isMultipleValues) { // This is a multi-value field but not a dropdown $field.prop('disabled', true).addClass('synced-attribute').hide(); // Create a styled disabled field to show multiple values var $multiDisplay = $('<input type="text" class="multi-value-display" disabled>') .val(syncedValue) .insertAfter($field); // Add the sync badge $multiDisplay.after('<span class="wc-attributes-icon" data-tip="Synced from product attributes"></span>'); } } else if (isMultipleValues) { // For non-dropdown multi-value fields $field.val(syncedValue) .prop('disabled', true) .addClass('synced-attribute') .css({ 'cursor': 'not-allowed', 'background-color': '#f0f0f1', 'color': 'rgba(44, 51, 56, .5)' }) .show(); // Add the sync badge if it doesn't exist if ($field.next('.wc-attributes-icon').length === 0) { $field.after('<span class="sync-indicator wc-attributes-icon" data-tip="Synced from the Attributes tab." style="margin-left: 4px;"><span class="sync-tooltip">Synced from the Attributes tab.</span></span>'); } } else { // Single value fields that are dropdowns (age_group, gender, condition) $field.val(syncedValue) .prop('disabled', true) .addClass('synced-attribute') .css({ 'cursor': 'not-allowed', 'background-color': '#f0f0f1', 'color': 'rgba(44, 51, 56, .5)' }) .show(); // Add the sync badge if it doesn't exist if ($field.next('.wc-attributes-icon').length === 0) { $field.after('<span class="sync-indicator wc-attributes-icon" data-tip="Synced from the Attributes tab." style="margin-left: 4px;"><span class="sync-tooltip">Synced from the Attributes tab.</span></span>'); } } } else { // Standard fields with single values $field.val(syncedValue) .prop('disabled', true) .addClass('synced-attribute') .css({ 'cursor': 'not-allowed', 'background-color': '#f0f0f1', 'color': 'rgba(44, 51, 56, .5)' }) .show(); // Add the sync badge if it doesn't exist if ($field.next('.wc-attributes-icon').length === 0) { $field.after('<span class="sync-indicator wc-attributes-icon" data-tip="Synced from the Attributes tab." style="margin-left: 4px;"><span class="sync-tooltip">Synced from the Attributes tab.</span></span>'); } } // Mark this field as synced syncedFields[key] = true; syncedBadgeState[key] = true; } else { // If this field was previously synced but now isn't if (syncedFields[key]) { // Reset synced state syncedFields[key] = false; // Completely reset the field value resetFieldToDefault(fieldId); } else if (manualValues[key] && !$field.val()) { // Restore manual value if field is empty $field.val(manualValues[key]); } // Reset the badge state syncedBadgeState[key] = false; } }); } } }); } // Function to completely reset all fields after attribute removal function resetAllFields() { var fields = { 'material': '<?php echo esc_js( \WC_Facebook_Product::FB_MATERIAL ); ?>', 'color': '<?php echo esc_js( \WC_Facebook_Product::FB_COLOR ); ?>', 'size': '<?php echo esc_js( \WC_Facebook_Product::FB_SIZE ); ?>', 'pattern': '<?php echo esc_js( \WC_Facebook_Product::FB_PATTERN ); ?>', 'brand': '<?php echo esc_js( \WC_Facebook_Product::FB_BRAND ); ?>', 'mpn': '<?php echo esc_js( \WC_Facebook_Product::FB_MPN ); ?>', 'age_group': '<?php echo esc_js( \WC_Facebook_Product::FB_AGE_GROUP ); ?>', 'gender': '<?php echo esc_js( \WC_Facebook_Product::FB_GENDER ); ?>', 'condition': '<?php echo esc_js( \WC_Facebook_Product::FB_PRODUCT_CONDITION ); ?>' }; Object.keys(fields).forEach(function(key) { var fieldId = '#' + fields[key]; resetFieldToDefault(fieldId); syncedFields[key] = false; syncedBadgeState[key] = false; }); } // Store manual input values for text fields $('.woocommerce_options_panel input[type="text"]').on('input', function() { var fieldId = $(this).attr('id'); for (var key in syncedBadgeState) { if (fieldId && fieldId.includes(key)) { manualValues[key] = $(this).val(); // When manually entering a value, mark as not synced syncedFields[key] = false; } } }); // Store manual selection values for select fields $('.woocommerce_options_panel select').on('change', function() { var fieldId = $(this).attr('id'); for (var key in syncedBadgeState) { if (fieldId && fieldId.includes(key)) { manualValues[key] = $(this).val(); // When manually selecting a value, mark as not synced syncedFields[key] = false; } } }); // Listen for attribute removal $('.product_data_tabs').on('click', '.remove_row', function(e) { // Store information about which row was removed var $removedRow = $(this).closest('tr'); var attributeName = $removedRow.find('td.attribute_name').text().trim().toLowerCase(); // Wait a brief moment for WooCommerce to remove the attribute setTimeout(function() { // Clean up any extra UI elements that might be leftover $('.woocommerce_options_panel').find('.multi-value-display').each(function() { // For each multi-value display, check if there's a corresponding select field var $this = $(this); var $select = $this.prev('select'); // If no select exists or the select has no options, remove the multi-value display if ($select.length === 0 || $select.find('option').length <= 1) { $this.next('.sync-indicator').remove(); $this.remove(); } }); // Only trigger if we're on the Facebook tab if ($('.fb_commerce_tab').hasClass('active')) { // First reset all fields to ensure dropdowns are cleared resetAllFields(); // Then perform a complete cleanup of all UI elements $('.woocommerce_options_panel').find('.multi-value-display, .sync-indicator').remove(); // Re-check all select fields for emptiness $('.woocommerce_options_panel select').each(function() { if ($(this).find('option').length <= 1) { // Reset to first option for empty selects $(this).val('').prop('selected', true); // Make sure it's visible and enabled $(this).show().prop('disabled', false).removeClass('synced-attribute'); } }); // Then sync to update based on remaining attributes syncFacebookAttributes(); } }, 300); // Increased timeout to ensure WooCommerce has fully removed the attribute }); // Listen for attribute saves $(document).on('click', 'button.save_attributes', function() { // Store reference to the button and attributes panel var $button = $(this); var $attributesPanel = $('#product_attributes'); // Wait a brief moment for WooCommerce to save the attributes setTimeout(function() { // Perform cleanup of any stray elements across the entire form $('.woocommerce_options_panel').find('.multi-value-display, .sync-indicator').each(function() { var $element = $(this); var $prevSelect = $element.prev('select'); // If this is a multi-value display without a valid select, remove it if ($element.hasClass('multi-value-display') && (!$prevSelect.length || $prevSelect.find('option').length <= 1 || !$prevSelect.is(':visible'))) { $element.next('.sync-indicator').remove(); $element.remove(); } // If this is a sync indicator without a valid field before it, remove it if ($element.hasClass('sync-indicator') && (!$element.prev().length || ($element.prev().is('select') && $element.prev().find('option').length <= 1))) { $element.remove(); } }); // Re-check all select fields $('.woocommerce_options_panel select').each(function() { var $select = $(this); // Check for empty or nearly empty selects if ($select.find('option').length <= 1) { // Clean up any associated UI elements $select.next('.multi-value-display').next('.sync-indicator').remove(); $select.next('.multi-value-display').remove(); $select.next('.sync-indicator').remove(); // Reset the select $select.val('').prop('selected', true) .show().prop('disabled', false).removeClass('synced-attribute'); } }); // Only trigger if we're on the Facebook tab if ($('.fb_commerce_tab').hasClass('active')) { syncFacebookAttributes(); } }, 500); }); // Function to clean up all UI elements and empty dropdowns function cleanupAllUIElements() { // Remove all multi-value displays and sync indicators $('.woocommerce_options_panel').find('.multi-value-display, .sync-indicator').remove(); // Reset all select fields $('.woocommerce_options_panel select').each(function() { var $select = $(this); $select.show().prop('disabled', false).removeClass('synced-attribute'); // If the select has no options ensure it's properly reset if ($select.find('option').length < 1) { $select.val('').prop('selected', true); } // Reset select2 if applicable if ($select.hasClass('wc-enhanced-select') || $select.hasClass('select2-hidden-accessible')) { try { $select.select2('val', ''); } catch (e) { // Ignore select2 errors } } }); // Reset all text inputs styling $('.woocommerce_options_panel input[type="text"]').each(function() { var $input = $(this); if ($input.hasClass('multi-value-display')) { $input.remove(); return; } $input.show().prop('disabled', false).removeClass('synced-attribute'); }); } // Original tab click handler $('.product_data_tabs li').on('click', function() { var tabClass = $(this).attr('class'); // If we're clicking on a tab that isn't the Facebook tab, // clean up all UI elements first if (!tabClass || !tabClass.includes('fb_commerce_tab')) { cleanupAllUIElements(); } else if (tabClass && tabClass.includes('fb_commerce_tab')) { // If we're clicking on the Facebook tab // First clean up any previous UI elements cleanupAllUIElements(); // Then sync to get the latest data syncFacebookAttributes(); } }); // Reset badge states when leaving the Facebook tab $('.product_data_tabs li').not('.fb_commerce_tab').on('click', function() { Object.keys(syncedBadgeState).forEach(function(key) { syncedBadgeState[key] = false; }); }); // Initial store of values Object.keys(syncedBadgeState).forEach(function(key) { var fieldId = '#fb_' + key; if (key === 'age_group') fieldId = '#' + '<?php echo esc_js( \WC_Facebook_Product::FB_AGE_GROUP ); ?>'; if (key === 'gender') fieldId = '#' + '<?php echo esc_js( \WC_Facebook_Product::FB_GENDER ); ?>'; if (key === 'condition') fieldId = '#' + '<?php echo esc_js( \WC_Facebook_Product::FB_PRODUCT_CONDITION ); ?>'; var $field = $(fieldId); var value = $field.val(); if (value && !$field.hasClass('synced-attribute')) { manualValues[key] = value; } }); // When the page loads, immediately sync if we're on the Facebook tab if ($('.fb_commerce_tab').hasClass('active')) { syncFacebookAttributes(); } }); </script> <?php } public function sync_product_attributes( $product_id ) { $product = wc_get_product( $product_id ); if ( ! $product ) { return []; } // Use the ProductAttributeMapper to get mapped attributes if ( class_exists( '\WooCommerce\Facebook\ProductAttributeMapper' ) ) { $mapped_attributes = \WooCommerce\Facebook\ProductAttributeMapper::get_and_save_mapped_attributes( $product ); return $mapped_attributes; } // Fallback to old method if ProductAttributeMapper is not available $attributes = $product->get_attributes(); $facebook_fields = []; $attribute_map = [ 'material' => \WC_Facebook_Product::FB_MATERIAL, 'color' => \WC_Facebook_Product::FB_COLOR, 'colour' => \WC_Facebook_Product::FB_COLOR, // Add support for British spelling 'size' => \WC_Facebook_Product::FB_SIZE, 'pattern' => \WC_Facebook_Product::FB_PATTERN, 'brand' => \WC_Facebook_Product::FB_BRAND, 'mpn' => \WC_Facebook_Product::FB_MPN, 'age_group' => \WC_Facebook_Product::FB_AGE_GROUP, 'gender' => \WC_Facebook_Product::FB_GENDER, 'condition' => \WC_Facebook_Product::FB_PRODUCT_CONDITION, ]; // Dropdown-based attributes that should match specific values $dropdown_attrs = [ 'age_group' => [ \WC_Facebook_Product::AGE_GROUP_ADULT, \WC_Facebook_Product::AGE_GROUP_ALL_AGES, \WC_Facebook_Product::AGE_GROUP_TEEN, \WC_Facebook_Product::AGE_GROUP_KIDS, \WC_Facebook_Product::AGE_GROUP_TODDLER, \WC_Facebook_Product::AGE_GROUP_INFANT, \WC_Facebook_Product::AGE_GROUP_NEWBORN, ], 'gender' => [ \WC_Facebook_Product::GENDER_MALE, \WC_Facebook_Product::GENDER_FEMALE, \WC_Facebook_Product::GENDER_UNISEX, ], 'condition' => [ \WC_Facebook_Product::CONDITION_NEW, \WC_Facebook_Product::CONDITION_USED, \WC_Facebook_Product::CONDITION_REFURBISHED, ], ]; // Process all attributes and track which have been processed $processed_fields = []; foreach ( $attributes as $attribute ) { // Get all possible variations of the attribute name for matching $raw_name = $attribute->get_name(); $clean_name = str_replace( 'pa_', '', $raw_name ); $normalized_attr_name = strtolower( $clean_name ); $attribute_label = wc_attribute_label( $raw_name ); $normalized_label = strtolower( $attribute_label ); // Create variations for more flexible matching $name_variations = [ $normalized_attr_name, $normalized_label, str_replace( [ '_', ' ', '-' ], '', $normalized_attr_name ), str_replace( [ '_', ' ', '-' ], '', $normalized_label ), ]; // Find matching Facebook field $matched_facebook_field = null; $field_name = null; // Look for matches in attribute map foreach ( $attribute_map as $fb_attr_name => $fb_meta_key ) { $fb_variations = [ $fb_attr_name, str_replace( [ '_', ' ', '-' ], '', $fb_attr_name ), ]; // Check for any variation match $matched = false; foreach ( $name_variations as $name_var ) { foreach ( $fb_variations as $fb_var ) { if ( $name_var === $fb_var ) { $matched = true; $matched_facebook_field = $fb_meta_key; $field_name = $fb_attr_name; break 2; } } } if ( $matched ) { break; } } // Special case for color/colour conversion if ( 'colour' === $field_name ) { $field_name = 'color'; } // If we found a match and haven't processed this field yet if ( $matched_facebook_field && ! in_array( $field_name, $processed_fields, true ) ) { $values = []; if ( is_object( $attribute ) && method_exists( $attribute, 'is_taxonomy' ) && $attribute->is_taxonomy() ) { $terms = $attribute->get_terms(); if ( $terms && ! is_wp_error( $terms ) ) { $values = wp_list_pluck( $terms, 'name' ); } } else { $values = $attribute->get_options(); } if ( ! empty( $values ) ) { // For dropdown attributes, validate against allowed values if ( array_key_exists( $field_name, $dropdown_attrs ) ) { $valid_values = []; foreach ( $values as $value ) { $normalized_value = strtolower( trim( $value ) ); foreach ( $dropdown_attrs[ $field_name ] as $allowed_value ) { if ( strtolower( $allowed_value ) === $normalized_value ) { $valid_values[] = $allowed_value; break; } } } if ( ! empty( $valid_values ) ) { $joined_values = implode( ' | ', $valid_values ); $facebook_fields[ $field_name ] = $joined_values; update_post_meta( $product_id, $matched_facebook_field, $joined_values ); } else { delete_post_meta( $product_id, $matched_facebook_field ); $facebook_fields[ $field_name ] = ''; } } else { // Regular attributes - join multiple values with a pipe character and spaces $joined_values = implode( ' | ', $values ); $facebook_fields[ $field_name ] = $joined_values; update_post_meta( $product_id, $matched_facebook_field, $joined_values ); } } else { delete_post_meta( $product_id, $matched_facebook_field ); $facebook_fields[ $field_name ] = ''; } // Mark this field as processed $processed_fields[] = $field_name; } } return $facebook_fields; } public function ajax_sync_facebook_attributes() { check_ajax_referer( 'sync_facebook_attributes', 'nonce' ); $product_id = isset( $_POST['product_id'] ) ? intval( $_POST['product_id'] ) : 0; if ( $product_id ) { $synced_fields = $this->sync_product_attributes( $product_id ); wp_send_json_success( $synced_fields ); } wp_send_json_error( 'Invalid product ID' ); } }
Copyright ©2021 || Defacer Indonesia