HEX
Server: Apache/2.4.65 (Debian)
System: Linux kubikelcreative 5.10.0-35-amd64 #1 SMP Debian 5.10.237-1 (2025-05-19) x86_64
User: www-data (33)
PHP: 8.4.13
Disabled: NONE
Upload Files
File: /var/www/indoadvisory/wp/wp-content/plugins/polylang-wc/plugins/mix-match.php
<?php
/**
 * @package Polylang-WC
 */

/**
 * Manages the compatibility with Mix and Match Products.
 * Version tested: 2.0.0.
 *
 * It handles the synchronization of products metas
 * and the translation of the cart when the language is switched.
 *
 * @since 1.1
 * @since 1.7 Added support for version 2.0+. Thanks @helgatheviking for bringing it.
 */
class PLLWC_Mix_Match {

	/**
	 * Using 2.0-style MNM tables.
	 *
	 * @var bool
	 */
	private $has_custom_db;

	/**
	 * An array of translated cart keys.
	 *
	 * @var array
	 */
	private $translated_cart_keys = array();

	/**
	 * Constructor.
	 * Setup filters.
	 *
	 * @since 1.1
	 */
	public function __construct() {
		$this->has_custom_db = class_exists( 'WC_MNM_Compatibility' ) && WC_MNM_Compatibility::is_db_version_gte( '2.0' );

		// Product synchronization.
		add_filter( 'pllwc_copy_post_metas', array( $this, 'copy_product_metas' ) );
		add_filter( 'pllwc_translate_product_meta', array( $this, 'translate_product_meta' ), 10, 3 );

		if ( $this->has_custom_db ) {
			add_action( 'pllwc_copy_product', array( $this, 'copy_product' ), 10, 4 );
		}

		// Cart.
		add_filter( 'pllwc_translate_cart_item', array( $this, 'translate_cart_item' ), 10, 2 );
		add_filter( 'pllwc_add_cart_item_data', array( $this, 'add_cart_item_data' ), 10, 2 );
		add_action( 'pllwc_translated_cart_item', array( $this, 'translated_cart_item' ), 10, 2 );
		add_filter( 'pllwc_translate_cart_contents', array( $this, 'translate_cart_contents' ) );
		add_action( 'woocommerce_cart_loaded_from_session', array( $this, 'cart_loaded_from_session' ), 20 ); // After PLLWC_Frontend_Cart.
	}

	/**
	 * Adds metas to synchronize when saving a product.
	 * Hooked to the filter 'pllwc_copy_post_metas'.
	 *
	 * @since 1.1
	 *
	 * @param string[] $metas List of custom fields names.
	 * @return string[]
	 */
	public function copy_product_metas( $metas ) {
		if ( $this->has_custom_db ) {
			return array_merge(
				$metas,
				array(
					'_mnm_base_price'                => '_mnm_base_price',
					'_mnm_base_regular_price'        => '_mnm_base_regular_price',
					'_mnm_base_sale_price'           => '_mnm_base_sale_price',
					'_mnm_max_container_size'        => '_mnm_max_container_size',
					'_mnm_min_container_size'        => '_mnm_min_container_size',
					'_mnm_per_product_pricing'       => '_mnm_per_product_pricing',
					// Only M&M >= 2.0.
					'_mnm_add_to_cart_form_location' => '_mnm_add_to_cart_form_location',
					'_mnm_child_category_ids'        => '_mnm_child_category_ids',
					'_mnm_content_source'            => '_mnm_content_source',
					'_mnm_layout_override'           => '_mnm_layout_override',
					'_mnm_layout_style'              => '_mnm_layout_style',
					'_mnm_packing_mode'              => '_mnm_packing_mode',
					'_mnm_per_product_discount'      => '_mnm_per_product_discount',
					'_mnm_weight_cumulative'         => '_mnm_weight_cumulative',
				)
			);
		} else {
			return array_merge(
				$metas,
				array(
					'_mnm_base_price'           => '_mnm_base_price',
					'_mnm_base_regular_price'   => '_mnm_base_regular_price',
					'_mnm_base_sale_price'      => '_mnm_base_sale_price',
					'_mnm_max_container_size'   => '_mnm_max_container_size',
					'_mnm_min_container_size'   => '_mnm_min_container_size',
					'_mnm_per_product_pricing'  => '_mnm_per_product_pricing',
					// Only M&M < 2.0.
					'_mnm_data'                 => '_mnm_data',
					'_mnm_per_product_shipping' => '_mnm_per_product_shipping',
				)
			);
		}
	}

	/**
	 * Translates the Mix and Match contents.
	 * Hooked to the filter 'pllwc_translate_product_meta'.
	 *
	 * @since 1.1
	 *
	 * @param  mixed  $value Meta value.
	 * @param  string $key   Meta key.
	 * @param  string $lang  Language of target.
	 * @return mixed
	 */
	public function translate_product_meta( $value, $key, $lang ) {
		switch ( $key ) {
			case '_mnm_child_category_ids':
				// For MNM 2.x category contents.
				if ( empty( $value ) || ! is_array( $value ) ) {
					// An array of IDs is expected.
					return array();
				}

				$out = array();

				foreach ( $value as $category_id ) {
					if ( ! is_numeric( $category_id ) || $category_id <= 0 ) {
						continue;
					}

					$out[] = pll_get_term( (int) $category_id, $lang );
				}

				$value = array_filter( $out );
				break;

			case '_mnm_data':
				/**
				 * Backward compatibility for MNM 1.x. As of 2.0 child items are stored in custom DB table.
				 *
				 * @see: PLLWC_Mix_Match::copy_product()
				 */
				if ( empty( $value ) || ! is_array( $value ) ) {
					// An array of IDs is expected.
					return array();
				}

				$out        = array();
				$data_store = PLLWC_Data_Store::load( 'product_language' );

				foreach ( $value as $post_id => $data ) {
					if ( ! is_numeric( $post_id ) || $post_id <= 0 ) {
						continue;
					}

					$tr_id = $data_store->get( $post_id, $lang );

					if ( empty( $tr_id ) ) {
						$out[ $post_id ] = $data;
						continue;
					}

					$tr_product_id   = $tr_id;
					$tr_variation_id = 0;

					// If a variation, need to also translate the parent ID.
					if ( ! empty( $data['product_id'] ) && ! empty( $data['variation_id'] ) ) {
						$tr_product_id   = $data_store->get( $data['product_id'], $lang );
						$tr_variation_id = $tr_id;
					}

					$out[ $tr_id ] = array(
						'child_id'     => $tr_id,
						'product_id'   => $tr_product_id,
						'variation_id' => $tr_variation_id,
					);
				}

				$value = $out;
				break;
		}

		return $value;
	}

	/**
	 * Copies or synchronizes the bundled items.
	 * Hooked to the action 'pllwc_copy_product'.
	 *
	 * @since 1.7
	 *
	 * @param int    $from Id of the post from which we copy information.
	 * @param int    $to   Id of the post to which we paste information.
	 * @param string $lang language slug.
	 * @return void
	 */
	public function copy_product( $from, $to, $lang ) {
		/**
		 * Used to prevent an infinite loop.
		 *
		 * @var array<int,int> Post IDs as array keys. 1 as values
		 */
		static $copying_products = array();

		if ( isset( $copying_products[ $from ] ) ) {
			// Prevent an infinite loop.
			return;
		}

		/** @var WC_Product_Mix_and_Match|null|false $from_product */
		$from_product = wc_get_product( $from );

		if ( empty( $from_product ) || ! $from_product->is_type( 'mix-and-match' ) ) {
			return;
		}

		$to_product = new WC_Product_Mix_and_Match( $to );

		if ( empty( $to_product ) ) {
			return;
		}

		$copying_products[ $from ] = 1;

		/** @var PLLWC_Product_Language_CPT $data_store */
		$data_store = PLLWC_Data_Store::load( 'product_language' );
		$tr_items   = array();

		foreach ( $from_product->get_child_items() as $item ) {
			$tr_product_id = $data_store->get( $item->get_product_id(), $lang );

			if ( $item->get_variation_id() ) {
				$tr_variation_id = $data_store->get( $item->get_variation_id(), $lang );

				if ( $tr_product_id && $tr_variation_id ) {
					$tr_items[] = array(
						'product_id'   => $tr_product_id,
						'variation_id' => $tr_variation_id,
					);
				}
			} elseif ( $tr_product_id ) {
				$tr_items[] = array(
					'product_id'   => $tr_product_id,
					'variation_id' => 0,
				);
			}
		}

		if ( ! empty( $tr_items ) ) {
			$to_product->set_child_items( $tr_items );
			$to_product->save();
		}

		unset( $copying_products[ $from ] );
	}

	/**
	 * Translates items in the cart.
	 * Hooked to the filter 'pllwc_translate_cart_item'.
	 *
	 * @since 1.1
	 *
	 * @param array  $item Cart item.
	 * @param string $lang Language code.
	 * @return array
	 */
	public function translate_cart_item( $item, $lang = '' ) {
		// `wc_mnm_is_container_cart_item()` and `wc_mnm_maybe_is_child_cart_item()` were introoduced in M&M 1.7.
		if ( ! function_exists( 'wc_mnm_is_container_cart_item' ) ) {
			return $item;
		}

		if ( wc_mnm_is_container_cart_item( $item ) ) {
			$item['mnm_config'] = $this->translate_config( $item['mnm_config'], $lang );

			if ( isset( $item['mnm_contents'] ) ) {
				// Stash the content keys for later. Cannot translate now as the child products have not yet been translated.
				$item['mnm_contents_tr'] = $item['mnm_contents'];
				$item['mnm_contents']    = array();
			}
		} elseif ( wc_mnm_maybe_is_child_cart_item( $item ) ) {
			if ( isset( $item['mnm_container'], $this->translated_cart_keys[ $item['mnm_container'] ] ) ) {
				$item['mnm_container'] = $this->translated_cart_keys[ $item['mnm_container'] ];
			}
		}

		return $item;
	}

	/**
	 * Translates the config in the cart item.
	 *
	 * @since 1.7
	 *
	 * @param  array  $config Config.
	 * @param  string $lang   Language code.
	 * @return array<int<1,max>,array{
	 *     product_id: int<1,max>,
	 *     variation_id?: int<1,max>,
	 *     variation?: array<string,string>
	 * }>
	 */
	protected function translate_config( $config, $lang ) {
		/** @var PLLWC_Product_Language_CPT $data_store */
		$data_store = PLLWC_Data_Store::load( 'product_language' );
		$tr_config  = array();

		foreach ( $config as $row ) {
			$row['product_id'] = $data_store->get( $row['product_id'], $lang );

			if ( empty( $row['product_id'] ) ) {
				continue;
			}

			// Variations.
			if ( ! empty( $row['variation_id'] ) ) {
				$row['variation_id'] = $data_store->get( $row['variation_id'], $lang );

			}

			// Variations attributes.
			if ( ! empty( $row['variation'] ) ) {
				$orig_lang = $data_store->get_language( $row['product_id'] );

				if ( ! empty( $orig_lang ) ) {
					$row['variation'] = PLLWC()->cart->translate_attributes_in_cart( $row['variation'], $lang, $orig_lang );
				}
			}

			$tr_config[ $row['product_id'] ] = $row;
		}

		return $tr_config;
	}

	/**
	 * Adds Mix and Match Product information to the cart item data when translated.
	 * Hooked to the filter 'pllwc_add_cart_item_data'.
	 *
	 * @since 1.1
	 *
	 * @param array $cart_item_data Cart item data.
	 * @param array $item           Cart item.
	 * @return array
	 */
	public function add_cart_item_data( $cart_item_data, $item ) {
		$keys = array(
			'mnm_config',
			'mnm_contents',
			'mnm_container',
		);
		return array_merge( $cart_item_data, array_intersect_key( $item, array_flip( $keys ) ) );
	}

	/**
	 * Stores new cart keys as function of previous values.
	 * Later needed to restore the relationship between the Mix and Match product and contained products.
	 * Hooked to the action 'pllwc_translated_cart_item'.
	 *
	 * @since 1.1
	 *
	 * @param array  $item Cart item.
	 * @param string $key  Previous cart item key. The new key can be found in $item['key'].
	 * @return void
	 */
	public function translated_cart_item( $item, $key ) {
		$this->translated_cart_keys[ $key ] = $item['key'];
	}

	/**
	 * Assigns correct mnm_contents values to the Mix and Match product
	 * once the contained cart items have been translated.
	 * Hooked to the filter pllwc_translate_cart_contents.
	 *
	 * @since 1.1
	 *
	 * @param array $contents Cart contents.
	 * @return array
	 */
	public function translate_cart_contents( $contents ) {
		if ( empty( $this->translated_cart_keys ) ) {
			return $contents;
		}

		foreach ( $contents as $key => $cart_item ) {
			if ( ! wc_mnm_is_container_cart_item( $cart_item ) || empty( $cart_item['mnm_contents_tr'] ) ) {
				continue;
			}

			$contents[ $key ]['mnm_contents'] = array_unique(
				array_keys(
					array_intersect(
						array_flip( $this->translated_cart_keys ),
						$cart_item['mnm_contents_tr']
					)
				)
			);
			unset( $contents[ $key ]['mnm_contents_tr'] );
		}

		return $contents;
	}

	/**
	 * Allows WooCommerce Mix and Match to filter the cart prices after the cart has been translated.
	 * We need to do it here as WooCommerce Mix and Match directly access to WC()->cart->cart_contents.
	 * Hooked to the action 'woocommerce_cart_loaded_from_session'.
	 *
	 * @since 1.1
	 *
	 * @return void
	 */
	public function cart_loaded_from_session() {
		$mnm_cart = WC_Mix_and_Match_Cart::get_instance();

		foreach ( WC()->cart->cart_contents as $cart_key => $item ) {
			if ( empty( $item['data'] ) ) {
				continue;
			}

			WC()->cart->cart_contents[ $cart_key ] = $mnm_cart->add_cart_item_filter( $item, $cart_key );
		}
	}
}