File: /var/www/indoadvisory/wp/wp-content/plugins/polylang-wc/include/product-language-cpt.php
<?php
/**
* @package Polylang-WC
*/
defined( 'ABSPATH' ) || exit;
/**
* Setups the product languages and translations model when products are managed with a custom post type.
*
* @since 1.0
*/
class PLLWC_Product_Language_CPT extends PLLWC_Translated_Object_Language {
/**
* Properties stored in the table `wc_product_meta_lookup`.
*/
const META_LOOKUP_PROPS = array(
'sku',
'global_unique_id',
'regular_price',
'sale_price',
'date_on_sale_from',
'date_on_sale_to',
'total_sales',
'average_rating',
'stock_quantity',
'stock_status',
'manage_stock',
'downloadable',
'virtual',
'tax_status',
'tax_class',
);
/**
* WooCommerce permalinks option.
*
* @var string[]
*/
protected $permalinks;
/**
* Current attribute term being edited.
*
* @var WP_Term|null
*/
protected static $editing_term;
/**
* Product data store.
*
* @var PLLWC_Product_Data_Store_CPT
*/
protected $data_store;
/**
* Constructor.
*
* @since 1.5
*/
public function __construct() {
$this->object = PLL()->model->post;
$this->data_store = new PLLWC_Product_Data_Store_CPT();
}
/**
* Adds hooks.
*
* @since 1.0
* @since 1.9 Returns an instance of this object.
*
* @return self
*/
public function init() {
$this->permalinks = get_option( 'woocommerce_permalinks' );
add_filter( 'pll_get_post_types', array( $this, 'translated_post_types' ), 10, 2 );
add_filter( 'woocommerce_register_post_type_product', array( $this, 'woocommerce_register_post_type_product' ) );
add_filter( 'woocommerce_variable_children_args', array( $this, 'variable_children_args' ) );
// Synchronization.
add_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 4 );
add_filter( 'pll_copy_post_metas', array( $this, 'copy_post_metas' ), 5, 5 );
add_filter( 'pll_translate_post_meta', array( $this, 'translate_post_meta' ), 5, 5 );
add_action( 'woocommerce_product_object_updated_props', array( $this, 'updated_props' ), 10, 2 );
add_action( 'pll_post_synchronized', array( $this, 'post_synchronized' ), 10, 2 );
// Attributes.
remove_action( 'edit_term', array( 'WC_Post_Data', 'edit_term' ) );
remove_action( 'edited_term', array( 'WC_Post_Data', 'edited_term' ) );
add_action( 'edit_term', array( $this, 'edit_term' ), 10, 3 );
add_action( 'edited_term', array( $this, 'edited_term' ), 10, 3 );
return $this;
}
/**
* Add products and variations to translated post types.
*
* @since 1.0
*
* @param string[] $types List of post type names for which Polylang manages language and translations.
* @param bool $hide True when displaying the list in Polylang settings.
* @return string[] List of post type names for which Polylang manages language and translations.
*/
public function translated_post_types( $types, $hide ) {
$woo_types = array( 'product', 'product_variation' );
return $hide ? array_diff( $types, $woo_types ) : array_merge( $types, $woo_types );
}
/**
* Disables the translation of the product slug handled by WooCommerce as it does not play nice in multilingual context.
*
* @since 0.1
*
* @param array $args arguments used to register the post type.
* @return array
*/
public function woocommerce_register_post_type_product( $args ) {
$product_permalink = empty( $this->permalinks['product_base'] ) ? 'product' : $this->permalinks['product_base'];
$args['rewrite'] = array( 'slug' => untrailingslashit( $product_permalink ), 'with_front' => false, 'feeds' => true );
return $args;
}
/**
* Filters args used to get variable products children, to make sure that they are not filtered by the current language.
*
* @since 0.7.3
*
* @param array $args Query arguments.
* @return array
*/
public function variable_children_args( $args ) {
$args['lang'] = '';
return $args;
}
/**
* When assigning a product type, clear the cached value.
*
* @since 1.3.3
*
* @param int $object_id Object ID.
* @param WP_Term[] $terms An array of object terms.
* @param int[] $tt_ids An array of term taxonomy IDs.
* @param string $taxonomy Taxonomy slug.
* @return void
*/
public function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy ) {
if ( 'product_type' === $taxonomy ) {
$cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $object_id ) . '_type_' . $object_id;
wp_cache_delete( $cache_key, 'products' );
}
}
/**
* Returns legacy product metas mapped to product properties.
*
* @since 1.0
* @since 1.9 Type-hinted.
*
* @param bool $sync True if it is synchronization, false if it is a copy.
* @return string[]
*/
protected function get_legacy_metas( $sync ): array {
$metas_to_sync = array(
'_backorders' => 'backorders',
'_children' => 'children',
'_crosssell_ids' => 'cross_sell_ids',
'_default_attributes' => 'default_attributes',
'_download_expiry' => 'download_expiry',
'_download_limit' => 'download_limit',
'_downloadable' => 'downloadable',
'_downloadable_files' => 'downloads',
'_featured' => 'featured',
'_height' => 'height',
'_length' => 'length',
'_low_stock_amount' => 'low_stock_amount',
'_manage_stock' => 'manage_stock',
'_price' => 'price',
'_product_attributes' => 'attributes',
'_product_image_gallery' => 'gallery_image_ids',
'_regular_price' => 'regular_price',
'_sale_price' => 'sale_price',
'_sale_price_dates_from' => 'date_on_sale_from',
'_sale_price_dates_to' => 'date_on_sale_to',
'_sku' => 'sku',
'_global_unique_id' => 'global_unique_id',
'_sold_individually' => 'sold_individually',
'_stock' => 'stock_quantity',
'_tax_class' => 'tax_class',
'_tax_status' => 'tax_status',
'_thumbnail_id' => 'image_id',
'_upsell_ids' => 'upsell_ids',
'_virtual' => 'virtual',
'_weight' => 'weight',
'_width' => 'width',
'_button_text' => 'button_text',
'_product_url' => 'product_url',
'_purchase_note' => 'purchase_note',
'_variation_description' => 'description',
);
return $sync ? $metas_to_sync : array_merge(
$metas_to_sync,
array( '_stock_status' => 'stock_status' )
);
}
/**
* Get the custom fields to copy or synchronize.
*
* @since 1.0
*
* @param string[] $metas List of custom fields names.
* @param bool $sync True if it is synchronization, false if it is a copy.
* @param int $from Id of the product from which we copy information.
* @param int $to Id of the product to which we copy information.
* @param string $lang Language code.
* @return string[]
*/
public function copy_post_metas( $metas, $sync, $from, $to, $lang ) {
if ( ! in_array( get_post_type( $from ), array( 'product', 'product_variation' ) ) ) {
return $metas;
}
if ( $sync ) {
// Remove product attributes from metas to copy.
// Because these product post metas are public and automatically synchronized by Polylang.
// See https://github.com/polylang/polylang/blob/3.3.2/modules/sync/sync-post-metas.php#L51-L61.
$attributes_to_keep = preg_grep( '/^attribute_/', $metas, PREG_GREP_INVERT );
if ( false !== $attributes_to_keep ) {
$metas = $attributes_to_keep;
}
}
$_to_copy = self::get_legacy_metas( $sync );
$to_copy = array_keys( $_to_copy );
// Should we copy text?
if ( ! PLLWC_Products::should_copy_texts( $from, $to, $sync ) ) {
$to_copy = array_diff(
$to_copy,
array(
'_button_text',
'_product_url',
'_purchase_note',
'_variation_description',
)
);
}
/**
* Filters the product custom fields to copy or synchronize.
*
* @since 0.2
*
* @param string[] $to_copy List of custom fields names.
* @param bool $sync True if it is synchronization, false if it is a copy.
* @param int $from Id of the product from which we copy information.
* @param int $to Id of the product to which we paste information.
* @param string $lang Language code.
*/
$to_copy = array_unique( apply_filters( 'pllwc_copy_post_metas', array_combine( $to_copy, $to_copy ), $sync, $from, $to, $lang ) );
$metas = array_merge( $metas, $to_copy );
return $metas;
}
/**
* Update a lookup table for an object.
*
* @since 1.2
*
* @param int $id ID of object to update.
* @param string $table Lookup table name.
* @return void
*/
public function update_lookup_table( $id, $table ) {
$this->data_store->wc_update_lookup_table( $id, $table );
}
/**
* Fires actions and update look tables of translated products after properties have been synchronized.
*
* @since 1.2
*
* @param WC_Product|false $product Product.
* @param array $updated_props Product properties being updated.
* @return void
*/
public function updated_props( $product, $updated_props ) {
static $avoid_recursion = false;
if ( $avoid_recursion || ! $product ) {
return;
}
$avoid_recursion = true;
$id = $product->get_id();
foreach ( $this->get_translations( $id ) as $tr_id ) {
if ( $id && $id !== $tr_id && $tr_product = wc_get_product( $tr_id ) ) {
if ( in_array( 'stock_quantity', $updated_props, true ) ) {
if ( $tr_product->is_type( 'variation' ) ) {
do_action( 'woocommerce_variation_set_stock', $tr_product );
} else {
do_action( 'woocommerce_product_set_stock', $tr_product );
}
}
if ( array_intersect( $updated_props, self::META_LOOKUP_PROPS ) ) {
$this->update_lookup_table( $tr_product->get_id(), 'wc_product_meta_lookup' );
}
// Trigger action so 3rd parties can deal with updated props.
do_action( 'woocommerce_product_object_updated_props', $tr_product, $updated_props );
}
}
$avoid_recursion = false;
}
/**
* Updates the lookup table after product is duplicated or synchronized.
*
* @since 1.5.5
*
* @param int $from Id of the product from which we copy information.
* @param int $to Id of the target.
* @return void
*/
public function post_synchronized( $from, $to ) {
if ( in_array( get_post_type( $to ), array( 'product', 'product_variation' ) ) ) {
$this->update_lookup_table( $to, 'wc_product_meta_lookup' );
}
}
/**
* Translates a custom field before it is copied or synchronized.
*
* @since 1.0
*
* @param mixed $value Meta value.
* @param string $key Meta key.
* @param string $lang Language of target.
* @param int $from Id of the object from which we copy information.
* @param int $to Id of the target.
* @return mixed
*/
public function translate_post_meta( $value, $key, $lang, $from, $to ) {
if ( in_array( get_post_type( $from ), array( 'product', 'product_variation' ) ) ) {
if ( 0 === strpos( $key, 'attribute_' ) ) {
// Translate taxonomy attributes in variations.
$tax = substr( $key, 10 );
if ( ! empty( $tax ) && ! empty( $value ) && is_string( $value ) ) {
$attribute = array( $tax => $value );
$tr_attribute = PLLWC_Products::maybe_translate_attributes( $attribute, $lang );
$value = $tr_attribute[ $tax ];
}
} else {
$props = self::get_legacy_metas( false );
if ( isset( $props[ $key ] ) ) {
$value = PLLWC_Products::maybe_translate_property( $value, $props[ $key ], $lang );
}
}
/**
* Filters a meta value before is copied or synchronized.
*
* @since 1.0
*
* @param mixed $value Meta value.
* @param string $key Meta key.
* @param string $lang Language of target.
* @param int $from Id of the source.
* @param int $to Id of the target.
*/
$value = apply_filters( 'pllwc_translate_product_meta', $value, $key, $lang, $from, $to );
}
return $value;
}
/**
* Copies properties (taxonomies and metas) from one product to another product.
*
* @since 1.0
*
* @param int $from Id of the product from which we copy information.
* @param int $to Id of the product to which we copy information.
* @param string $lang Language code.
* @param bool $sync Optional, defaults to false. True if it is synchronization, false if it is a copy.
* @return void
*/
public function copy( $from, $to, $lang, $sync = false ) {
if ( isset( PLL()->sync ) ) {
if ( ! $sync ) {
PLL()->sync->taxonomies->copy( $from, $to, $lang );
}
PLL()->sync->post_metas->copy( $from, $to, $lang, $sync );
}
// Since WC 3.6 we also need to update the lookup table.
$this->update_lookup_table( $to, 'wc_product_meta_lookup' );
}
/**
* Synchronizes product ordering.
*
* @since 1.0
*
* @param int $id Product id.
* @param int $order Product order.
* @return void
*/
public function save_product_ordering( $id, $order ) {
global $wpdb;
$wpdb->update( $wpdb->posts, array( 'menu_order' => $order ), array( 'ID' => $id ) );
}
/**
* Returns the translation group name of a product (if it exists).
* This is the name of the associated taxonomy term.
*
* @since 1.0
*
* @param int $id Product id.
* @return string
*/
public function get_translation_group_name( $id ) {
$term = $this->object->get_object_term( $id, 'post_translations' );
return empty( $term ) ? '' : $term->name;
}
/**
* Checks if a field (product unique ID or SKU) is found in the database for any other product ID in a language.
* Modified version of `WC_Product_Data_Store_CPT::is_existing_sku()` and `WC_Product_Data_Store_CPT::is_existing_global_unique_id()`.
*
* @since 2.1
*
* @param string $column The field to search.
* @param string $value The field value to search (slashed to work around https://core.trac.wordpress.org/ticket/27421).
* @param int $product_id Product ID.
* @param PLL_Language $language The product language.
* @return bool
*/
public function lookup_table_has( string $column, string $value, int $product_id, PLL_Language $language ): bool {
if ( ! in_array( $column, self::META_LOOKUP_PROPS, true ) ) {
_doing_it_wrong(
__METHOD__,
esc_html( sprintf( 'The column name must be one of the columns in the `wc_product_meta_lookup` table: %s', implode( ', ', self::META_LOOKUP_PROPS ) ) ),
'2.1'
);
return false;
}
global $wpdb;
return (bool) $wpdb->get_var(
$wpdb->prepare(
"SELECT posts.ID
FROM {$wpdb->posts} AS posts
INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
INNER JOIN {$wpdb->term_relationships} AS pll_tr ON pll_tr.object_id = posts.ID
WHERE posts.post_type IN ( 'product', 'product_variation' )
AND posts.post_status != 'trash'
AND lookup.{$column} = %s" . // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
'AND lookup.product_id <> %d
AND pll_tr.term_taxonomy_id = %d
LIMIT 1',
wp_slash( $value ),
$product_id,
$language->get_tax_prop( 'language', 'term_taxonomy_id' )
)
);
}
/**
* Returns a product id based on the sku and the language.
* Modified version WC_Product_Data_Store_CPT::get_product_id_by_sku().
* Code last checked: WC 4.0
*
* @since 1.0
*
* @param string $sku SKU.
* @param string $lang Language code.
* @return int Product id.
*/
public function get_product_id_by_sku( $sku, $lang ) {
global $wpdb;
$lang = PLL()->model->get_language( $lang );
if ( empty( $lang ) ) {
return 0;
}
return $wpdb->get_var(
$wpdb->prepare(
"SELECT posts.ID
FROM {$wpdb->posts} as posts
INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id
INNER JOIN {$wpdb->term_relationships} AS pll_tr ON pll_tr.object_id = posts.ID
WHERE posts.post_type IN ( 'product', 'product_variation' )
AND posts.post_status != 'trash'
AND lookup.sku = %s
AND pll_tr.term_taxonomy_id = %d
LIMIT 1",
$sku,
$lang->get_tax_prop( 'language', 'term_taxonomy_id' )
)
);
}
/**
* When editing a term, check for product attributes.
*
* The method replaces WC_Post_Data::edit_term().
* A copy is needed because of the private property $editing_term.
* Code last checked: WC 4.0
*
* @since 1.2
*
* @param int $term_id Term ID.
* @param int $tt_id Term taxonomy ID.
* @param string $taxonomy Taxonomy slug.
* @return void
*/
public static function edit_term( $term_id, $tt_id, $taxonomy ) {
self::$editing_term = null;
if ( strpos( $taxonomy, 'pa_' ) === 0 ) {
$term = get_term_by( 'id', $term_id, $taxonomy );
if ( $term instanceof WP_Term ) {
self::$editing_term = $term;
}
}
}
/**
* When a term is edited, check for product attributes and update variations.
*
* This is a modified version of WC_Post_Data::edited_term().
* The language is added to the query to take into account updates of attributes sharing the same slug.
* Code last checked: WC 4.0
*
* @since 1.2
*
* @param int $term_id Term ID.
* @param int $tt_id Term taxonomy ID.
* @param string $taxonomy Taxonomy slug.
* @return void
*/
public function edited_term( $term_id, $tt_id, $taxonomy ) {
global $wpdb;
if ( ! is_null( self::$editing_term ) && strpos( $taxonomy, 'pa_' ) === 0 ) {
$edited_term = get_term_by( 'id', $term_id, $taxonomy );
if ( $edited_term instanceof WP_Term && $edited_term->slug !== self::$editing_term->slug ) {
$language = pll_get_term_language( $term_id, 'language:term_taxonomy_id' );
$language = absint( $language );
$wpdb->query(
$wpdb->prepare(
"UPDATE {$wpdb->postmeta} AS pm
INNER JOIN {$wpdb->term_relationships} AS pll_tr ON pll_tr.object_id = pm.post_id
SET meta_value = %s
WHERE meta_key = %s
AND meta_value = %s
AND pll_tr.term_taxonomy_id = %d",
$edited_term->slug,
'attribute_' . sanitize_title( $taxonomy ),
self::$editing_term->slug,
$language
)
);
$wpdb->query(
$wpdb->prepare(
"UPDATE {$wpdb->postmeta} AS pm
INNER JOIN {$wpdb->term_relationships} AS pll_tr ON pll_tr.object_id = pm.post_id
SET meta_value = REPLACE( meta_value, %s, %s )
WHERE meta_key = '_default_attributes'
AND pll_tr.term_taxonomy_id = %d",
serialize( self::$editing_term->taxonomy ) . serialize( self::$editing_term->slug ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
serialize( $edited_term->taxonomy ) . serialize( $edited_term->slug ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
$language
)
);
}
} else {
self::$editing_term = null;
}
}
}