2402 lines
91 KiB
PHP
2402 lines
91 KiB
PHP
<?php
|
|
|
|
/**
|
|
* WP Product Processing Support Trait.
|
|
*
|
|
* @package WP Product Feed Manager/Application/Functions
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
trait WPPFM_Processing_Support {
|
|
|
|
protected $_selected_number;
|
|
/** @var string|null */
|
|
private $_temp_tax_country = null;
|
|
/** @var string|null */
|
|
private $_temp_currency = null;
|
|
|
|
/**
|
|
* Cached "Use Performance Prioritizing" state for the current feed.
|
|
*
|
|
* @since 3.21.0
|
|
* @var bool|null Null means "not loaded yet".
|
|
*/
|
|
private $_wppfm_performance_enabled_cache = null;
|
|
|
|
/**
|
|
* Feed ID that the performance enabled cache belongs to.
|
|
*
|
|
* @since 3.21.0
|
|
* @var int|null
|
|
*/
|
|
private $_wppfm_performance_enabled_cache_feed_id = null;
|
|
|
|
|
|
|
|
/**
|
|
* Returns the correct category for this specific product, based on the selection in the Category Mapping table.
|
|
* This function supports the Yoast Primary Category.
|
|
*
|
|
* @param string $id the id of the product.
|
|
* @param string $main_category the selected Default Category.
|
|
* @param object $category_mapping containing the selected categories from the Category Mapping table.
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function get_mapped_category( $id, $main_category, $category_mapping ) {
|
|
$result = false;
|
|
$support_class = new WPPFM_Feed_Support();
|
|
$yoast_primary_category = WPPFM_Taxonomies::get_primary_cat( $id );
|
|
$yoast_cat_is_selected = $yoast_primary_category ? $support_class->selected_category_id( $yoast_primary_category[0]->term_id, $category_mapping ) : false;
|
|
|
|
$product_categories = $yoast_primary_category && false !== $yoast_cat_is_selected ? $yoast_primary_category :
|
|
wp_get_post_terms( $id, 'product_cat', array( 'taxonomy' => 'product_cat' ) ); // get the categories from a specific product in the shop
|
|
|
|
if ( $product_categories && ! is_wp_error( $product_categories ) ) {
|
|
// Loop through each category.
|
|
foreach ( $product_categories as $category ) {
|
|
// Check if this category is selected in the category mapping.
|
|
$shop_category_id = $support_class->selected_category_id( $category->term_id, $category_mapping );
|
|
|
|
// Only add this product when at least one of the categories is selected in the category mapping.
|
|
if ( false !== $shop_category_id ) {
|
|
//phpcs: ignore
|
|
switch ( $category_mapping[ $shop_category_id ]->feedCategories ) {
|
|
case 'wp_mainCategory':
|
|
$result = $main_category;
|
|
break;
|
|
|
|
case 'wp_ownCategory':
|
|
$result = WPPFM_Taxonomies::get_shop_categories( $id, ' > ' );
|
|
break;
|
|
|
|
default:
|
|
$result = $category_mapping[ $shop_category_id ]->feedCategories;
|
|
}
|
|
|
|
// Found a selected category so now return the result.
|
|
return $result; // Fixed ticket #1117.
|
|
|
|
} else { // If this product was not selected in the category mapping, it is possible it has been filtered in, so map it to the default category.
|
|
$result = $main_category;
|
|
}
|
|
}
|
|
} else {
|
|
if ( is_wp_error( $product_categories ) ) {
|
|
wppfm_handle_wp_errors_response(
|
|
$product_categories,
|
|
sprintf(
|
|
/* translators: %s: link to the support page */
|
|
__(
|
|
'2131 - Please try to refresh the page and open a support ticket at %s if the issue persists.',
|
|
'wp-product-feed-manager'
|
|
),
|
|
WPPFM_SUPPORT_PAGE_URL
|
|
)
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Checks if this product has been filtered out of the feed, based on a filter selection.
|
|
*
|
|
* @param string $feed_filter_strings the feed filter string.
|
|
* @param array $product_data an array with product data.
|
|
*
|
|
* @return boolean true if the product is filtered out.
|
|
*/
|
|
protected function is_product_filtered( $feed_filter_strings, $product_data ) {
|
|
if ( $feed_filter_strings ) {
|
|
return $this->filter_result( json_decode( $feed_filter_strings[0]['meta_value'] ), $product_data );
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the parent ids of a specific product.
|
|
*
|
|
* @param string $product_id the product id for which to look for parent ids.
|
|
*
|
|
* @return array with the parent ids.
|
|
*/
|
|
protected function get_product_parent_ids( $product_id ) {
|
|
$queries_class = new WPPFM_Queries();
|
|
|
|
$query_result = $queries_class->get_product_parents( $product_id );
|
|
$ids = array();
|
|
|
|
foreach ( $query_result as $result ) {
|
|
$ids[] = $result['ID'];
|
|
}
|
|
|
|
return $ids;
|
|
}
|
|
|
|
/**
|
|
* Extracts the column names of the selected sources, from a string that describes the selections for a specific feed attribute.
|
|
*
|
|
* @param string $value_string a string containing the source, condition and change value parameters.
|
|
*
|
|
* @return array with the source column names.
|
|
*/
|
|
protected function get_source_columns_from_attribute_value( $value_string ) {
|
|
$source_columns = array();
|
|
|
|
$value_object = json_decode( $value_string );
|
|
|
|
if ( $value_object && property_exists( $value_object, 'm' ) ) {
|
|
foreach ( $value_object->m as $source ) {
|
|
// TODO: I guess I should further reduce the "if" loops by combining them more then now
|
|
if ( is_object( $source ) && property_exists( $source, 's' ) ) {
|
|
if ( property_exists( $source->s, 'source' ) ) {
|
|
if ( 'combined' !== $source->s->source ) {
|
|
$source_columns[] = $source->s->source;
|
|
} else {
|
|
if ( property_exists( $source->s, 'f' ) ) {
|
|
$source_columns = array_merge( $source_columns, $this->get_combined_sources_from_combined_string( $source->s->f ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $source_columns;
|
|
}
|
|
|
|
/**
|
|
* Extracts the column names of the selected conditions, from a string that describes the selections for a specific feed attribute.
|
|
*
|
|
* @param string $value_string a string containing the source, condition and change value parameters.
|
|
*
|
|
* @return array with the condition column names.
|
|
*/
|
|
protected function get_condition_columns_from_attribute_value( $value_string ) {
|
|
$condition_columns = array();
|
|
|
|
$value_object = json_decode( $value_string );
|
|
|
|
if ( $value_object && property_exists( $value_object, 'm' ) ) {
|
|
foreach ( $value_object->m as $source ) {
|
|
if ( is_object( $source ) && property_exists( $source, 'c' ) ) {
|
|
for ( $i = 0; $i < count( $source->c ); $i ++ ) {
|
|
$condition_columns[] = $this->get_names_from_string( $source->c[ $i ]->{$i + 1} );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $condition_columns;
|
|
}
|
|
|
|
/**
|
|
* Extracts the column names of the selected queries, from a string that describes the selections for a specific feed attribute.
|
|
*
|
|
* @param string $value_string a string containing the source, condition and change value parameters.
|
|
*
|
|
* @return array with the query column names.
|
|
*/
|
|
protected function get_queries_columns_from_attribute_value( $value_string ) {
|
|
$query_columns = array();
|
|
|
|
$value_object = json_decode( $value_string );
|
|
|
|
if ( $value_object && property_exists( $value_object, 'v' ) ) {
|
|
foreach ( $value_object->v as $changed_value ) {
|
|
if ( property_exists( $changed_value, 'q' ) ) {
|
|
for ( $i = 0; $i < count( $changed_value->q ); $i ++ ) {
|
|
$query_columns[] = $this->get_names_from_string( $changed_value->q[ $i ]->{$i + 1} );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $query_columns;
|
|
}
|
|
|
|
/**
|
|
* Extract a column name from a string.
|
|
*
|
|
* @param string $string containing the column name.
|
|
*
|
|
* @return string with the column name.
|
|
*/
|
|
protected function get_names_from_string( $string ) {
|
|
$condition_string_array = explode( '#', $string );
|
|
|
|
return $condition_string_array[1];
|
|
}
|
|
|
|
/**
|
|
* Split the combined string into single combination items.
|
|
*
|
|
* @param string $combined_string the combined string.
|
|
*
|
|
* @return array containing the combination items.
|
|
*/
|
|
public function get_combined_sources_from_combined_string( $combined_string ) {
|
|
$result = array();
|
|
$combined_string_array = explode( '|', $combined_string );
|
|
|
|
$result[] = $combined_string_array[0];
|
|
|
|
for ( $i = 1; $i < count( $combined_string_array ); $i ++ ) {
|
|
$a = explode( '#', $combined_string_array[ $i ] );
|
|
if ( array_key_exists( 1, $a ) ) {
|
|
$result[] = $a[1];
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Gets the meta-data element of a specific attribute from the attribute's list.
|
|
*
|
|
* @param string $attribute the feed attribute name.
|
|
* @param stdClass $attributes the attribute's list.
|
|
*
|
|
* @return stdClass attribute class with metadata from the attribute.
|
|
*/
|
|
protected function get_meta_data_from_specific_attribute( $attribute, $attributes ) {
|
|
$i = 0;
|
|
|
|
while ( true ) {
|
|
if ( $attributes[ $i ]->fieldName !== $attribute ) {
|
|
$i ++;
|
|
if ( $i > 1000 ) {
|
|
break;
|
|
}
|
|
} else {
|
|
return $attributes[ $i ];
|
|
}
|
|
}
|
|
|
|
return new stdClass();
|
|
}
|
|
|
|
/**
|
|
* Generate the value of a field based on what the user has selected in filters, combined data, static data, e.g.,
|
|
*
|
|
* @param array $product_data contains the product data.
|
|
* @param stdClass $attribute_meta_data the meta-data of the product attribute.
|
|
* @param string $main_category_feed_title the main category title.
|
|
* @param string $row_category the processed Default Category for this product.
|
|
* @param string $feed_language selected language for the feed.
|
|
* @param string $feed_currency selected currency for the feed.
|
|
* @param array $relation_table a table containing the relation between attribute names and their db field names.
|
|
*
|
|
* @return array returns a key=>value array of a specific product field where the key contains the field name and the value the field value.
|
|
*/
|
|
protected function process_product_field( $product_data, $attribute_meta_data, $main_category_feed_title, $row_category, $feed_language, $feed_currency, $relation_table ) {
|
|
|
|
//@noinspection PhpVariableNameInspection
|
|
$product_object[ $attribute_meta_data->fieldName ] = $this->get_correct_field_value(
|
|
$attribute_meta_data,
|
|
$product_data,
|
|
$main_category_feed_title,
|
|
$row_category,
|
|
$feed_language,
|
|
$feed_currency,
|
|
$relation_table
|
|
);
|
|
|
|
return $product_object;
|
|
}
|
|
|
|
/**
|
|
* Processes a single field of a single field in the feed.
|
|
*
|
|
* @param stdClass $field_meta_data containing the meta-data of the field.
|
|
* @param array $product_data contains the product data.
|
|
* @param string $main_category_feed_title main category title.
|
|
* @param string $row_category the processed Default Category for this product.
|
|
* @param string $feed_language selected language for the feed.
|
|
* @param string $feed_currency selected currency for the feed.
|
|
* @param array $relation_table a table containing the relation between attribute names and their db field names.
|
|
*
|
|
* @return string containing the end value for the field.
|
|
*/
|
|
protected function get_correct_field_value( $field_meta_data, $product_data, $main_category_feed_title, $row_category, $feed_language, $feed_currency, $relation_table ) {
|
|
$this->_selected_number = 0;
|
|
|
|
// Do not process category strings, but only fields that are requested.
|
|
//phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
|
if ( property_exists( $field_meta_data, 'fieldName' )
|
|
&& $field_meta_data->fieldName !== $main_category_feed_title
|
|
&& $this->meta_data_contains_category_data( $field_meta_data ) === false ) {
|
|
|
|
$value_object = property_exists( $field_meta_data, 'value' ) && '' !== $field_meta_data->value ? json_decode( $field_meta_data->value ) : new stdClass();
|
|
|
|
if ( property_exists( $field_meta_data, 'value' )
|
|
&& '' !== $field_meta_data->value
|
|
&& property_exists( $value_object, 'm' ) ) { // seems to be something we need to work on
|
|
|
|
//phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
|
$advised_source = property_exists( $field_meta_data, 'advisedSource' ) ? $field_meta_data->advisedSource : '';
|
|
|
|
// Get the end value depending on the filter settings.
|
|
$end_row_value = $this->get_correct_end_row_value( $value_object->m, $product_data, $advised_source );
|
|
|
|
} else { // No queries, edit values or alternative sources for this field.
|
|
|
|
//phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
|
if ( property_exists( $field_meta_data, 'advisedSource' ) && '' !== $field_meta_data->advisedSource ) {
|
|
//phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
|
$db_title = $field_meta_data->advisedSource;
|
|
} else {
|
|
$support_class = new WPPFM_Feed_Support();
|
|
//phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
|
$source_title = $field_meta_data->fieldName;
|
|
$db_title = $support_class->get_db_column_title( $source_title, $relation_table );
|
|
}
|
|
|
|
$end_row_value = array_key_exists( $db_title, $product_data ) ? $product_data[ $db_title ] : '';
|
|
}
|
|
|
|
// Change value if requested.
|
|
if ( property_exists( $field_meta_data, 'value' ) && '' !== $field_meta_data->value && property_exists( $value_object, 'v' ) ) {
|
|
$pos = $this->_selected_number;
|
|
|
|
if ( property_exists( $value_object, 'm' ) && property_exists( $value_object->m[ $pos ], 's' ) ) {
|
|
$combination_string = property_exists( $value_object->m[ $pos ]->s, 'f' ) ? $value_object->m[ $pos ]->s->f : false;
|
|
$is_money = property_exists( $value_object->m[ $pos ]->s, 'source' ) && wppfm_meta_key_is_money( $value_object->m[ $pos ]->s->source );
|
|
} else {
|
|
$combination_string = false;
|
|
$is_money = false;
|
|
}
|
|
|
|
$row_value = ! $is_money ? $end_row_value : wppfm_prep_money_values( $end_row_value . $feed_language, $feed_currency );
|
|
$end_row_value = $this->get_edited_end_row_value( $value_object->v, $row_value, $product_data, $combination_string, $feed_language, $feed_currency );
|
|
}
|
|
} else {
|
|
$end_row_value = $row_category;
|
|
}
|
|
|
|
return $end_row_value;
|
|
}
|
|
|
|
/**
|
|
* Processes the selected source data of an attribute to get the end value for the attribute row that goes into the feed.
|
|
*
|
|
* @param object $source_selections object with a string that describes the source selection.
|
|
* @param array $product_data main product data.
|
|
* @param string $advised_source the advised source for this attribute. Empty of no advised source is active.
|
|
*
|
|
* @return string the end row value.
|
|
*/
|
|
private function get_correct_end_row_value( $source_selections, $product_data, $advised_source ) {
|
|
$end_row_value = '';
|
|
$nr_values = count( (array)$source_selections ); // added @since 1.9.4
|
|
$value_counter = 1; // added @since 1.9.4
|
|
|
|
foreach ( $source_selections as $source_selection ) {
|
|
if ( true === $this->get_filter_status( $source_selection, $product_data ) ) {
|
|
|
|
$end_row_value = $this->get_row_source_data( $source_selection, $product_data, $advised_source );
|
|
break;
|
|
} else {
|
|
// No "or else" value seems to be selected.
|
|
if ( $value_counter >= $nr_values ) {
|
|
return $end_row_value;
|
|
} // added @since 1.9.4
|
|
|
|
$this->_selected_number ++;
|
|
}
|
|
|
|
$value_counter ++; // added @since 1.9.4
|
|
}
|
|
|
|
// Not found a condition that was correct, so let's take the "for all other products" data to fetch the correct row_value.
|
|
if ( '' === $end_row_value ) {
|
|
$end_row_value = $this->get_row_source_data( end( $source_selections ), $product_data, $advised_source );
|
|
}
|
|
|
|
return $end_row_value;
|
|
}
|
|
|
|
/**
|
|
* Removes links from the post-content and post-excerpts in a product data array.
|
|
*
|
|
* @param array $product_data reference to the product data.
|
|
*
|
|
* @since 2.6.0.
|
|
*/
|
|
protected function remove_links_from_product_data_description( &$product_data ) {
|
|
$pattern = '#<a.*?>(.*?)</a>#i'; // link pattern
|
|
$replacement = '\1';
|
|
|
|
if ( array_key_exists( 'post_content', $product_data ) ) {
|
|
$product_data['post_content'] = preg_replace( $pattern, $replacement, $product_data['post_content'] );
|
|
}
|
|
|
|
if ( array_key_exists( 'post_excerpt', $product_data ) ) {
|
|
$product_data['post_excerpt'] = preg_replace( $pattern, $replacement, $product_data['post_excerpt'] );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the filter on this attribute excludes the product from the feed.
|
|
*
|
|
* @param object $source_selection containing the source title and a filter string.
|
|
* @param array $product_data with the product data.
|
|
*
|
|
* @return bool true if the filter does not exclude the product.
|
|
*/
|
|
private function get_filter_status( $source_selection, $product_data ) {
|
|
if ( ! empty( $source_selection ) && property_exists( $source_selection, 'c' ) ) {
|
|
// Check if the query is true for this field.
|
|
return $this->filter_result( $source_selection->c, $product_data );
|
|
} else {
|
|
// Apparently there is no condition, so the result is always true.
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles an array with conditions and checks if they are true for a specific product.
|
|
*
|
|
* @param array $conditions the array with conditions.
|
|
* @param array $product_data the product data.
|
|
*
|
|
* @return bool true if the condition is true for the specific product.
|
|
*/
|
|
private function filter_result( $conditions, $product_data ) {
|
|
$query_results = array();
|
|
$support_class = new WPPFM_Feed_Support();
|
|
|
|
foreach ( $conditions as $condition ) {
|
|
$condition_string = $support_class->get_query_string_from_query_object( $condition );
|
|
|
|
$query_split = explode( '#', $condition_string );
|
|
|
|
$row_result = $support_class->check_query_result_on_specific_row( $query_split, $product_data ) === true ? 'false' : 'true';
|
|
|
|
$query_results[] = $query_split[0] . '#' . $row_result;
|
|
}
|
|
|
|
return $this->combined_filter_result( $query_results );
|
|
}
|
|
|
|
/**
|
|
* Receives an array with condition results and generates a single end result based on the "and" or "or"
|
|
* connection between the conditions.
|
|
*
|
|
* @param array $results
|
|
*
|
|
* @return bool returns true if the combined filter (with "and" or "or" conditions) is true. False if not.
|
|
*/
|
|
private function combined_filter_result( $results ) {
|
|
$and_results = array();
|
|
$or_results = array();
|
|
|
|
if ( count( $results ) > 0 ) {
|
|
foreach ( $results as $query_result ) {
|
|
$result_split = explode( '#', $query_result );
|
|
|
|
if ( '2' === $result_split[0] ) {
|
|
$or_results[] = $and_results; // store the current "and" result for processing as "or" result
|
|
|
|
$and_results = array(); // clear the "and" array
|
|
}
|
|
|
|
$and_result = $result_split[1]; // === 'false' ? 'false' : 'true';
|
|
|
|
$and_results[] = $and_result;
|
|
}
|
|
|
|
if ( count( $and_results ) > 0 ) {
|
|
$or_results[] = $and_results;
|
|
}
|
|
|
|
$end_result = false;
|
|
|
|
if ( count( $or_results ) > 0 ) {
|
|
|
|
foreach ( $or_results as $or_result ) {
|
|
$a = true;
|
|
|
|
foreach ( $or_result as $and_array ) {
|
|
if ( 'false' === $and_array ) {
|
|
$a = false;
|
|
}
|
|
}
|
|
|
|
if ( $a ) {
|
|
$end_result = true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$end_result = false;
|
|
}
|
|
|
|
return $end_result;
|
|
}
|
|
|
|
/**
|
|
* Reads the source data of a row from the product data.
|
|
*
|
|
* @param object $filter contains the filter data.
|
|
* @param array $product_data with the product data.
|
|
* @param string $advised_source advised source if applicable.
|
|
*
|
|
* @return string with the source data.
|
|
*/
|
|
private function get_row_source_data( $filter, $product_data, $advised_source ) {
|
|
$row_source_data = '';
|
|
|
|
if ( ! empty( $filter ) && property_exists( $filter, 's' ) ) {
|
|
if ( property_exists( $filter->s, 'static' ) ) {
|
|
$row_source_data = $filter->s->static;
|
|
} elseif ( property_exists( $filter->s, 'source' ) ) {
|
|
if ( 'combined' !== $filter->s->source ) {
|
|
$row_source_data = array_key_exists( $filter->s->source, $product_data ) ? $product_data[ $filter->s->source ] : '';
|
|
} else {
|
|
$row_source_data = $this->generate_combined_source_output( $filter->s->f, $product_data );
|
|
}
|
|
}
|
|
} else {
|
|
// return the advised source data
|
|
if ( '' !== $advised_source ) {
|
|
$row_source_data = array_key_exists( $advised_source, $product_data ) ? $product_data[ $advised_source ] : '';
|
|
}
|
|
}
|
|
|
|
return $row_source_data;
|
|
}
|
|
|
|
/**
|
|
* Returns the end result of a combined string source selector.
|
|
*
|
|
* @param string $combined_sources a string with the selected combined sources.
|
|
* @param array $product_data the product data.
|
|
*
|
|
* @return string containing the output of the combined source selector.
|
|
*/
|
|
private function generate_combined_source_output( $combined_sources, $product_data ) {
|
|
$source_selectors_array = explode( '|', $combined_sources ); // Split the combined source string in an array containing every single source.
|
|
$values_class = new WPPFM_Feed_Value_Editors();
|
|
$separators = $values_class->combination_separators(); // Array with all possible separators.
|
|
|
|
// If one of the row results is an array, the final output needs to be an array.
|
|
$result_is_array = $this->check_if_any_source_has_array_data( $source_selectors_array, $product_data );
|
|
$result = $result_is_array ? array() : '';
|
|
|
|
if ( ! $result_is_array ) {
|
|
$result = $this->combine_the_outputs( $source_selectors_array, $separators, $product_data, false );
|
|
} else {
|
|
for ( $i = 0; $i < count( $result_is_array ); $i ++ ) {
|
|
$combined_string = $this->combine_the_outputs( $source_selectors_array, $separators, $product_data, $i );
|
|
$result[] = $combined_string;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Gets the keys from the $sources string (separated by a #), and it looks if any of these keys
|
|
* are linked to an array in the $data_row.
|
|
*
|
|
* @param array $sources with the source strings.
|
|
* @param array $product_data the product data.
|
|
*
|
|
* @return array|bool from the data_row or false if no array data is found.
|
|
*/
|
|
private function check_if_any_source_has_array_data( $sources, $product_data ) {
|
|
foreach ( $sources as $source ) {
|
|
$split_source = explode( '#', $source );
|
|
|
|
if ( count( $split_source ) > 1 && 'static' === $split_source[1] ) {
|
|
$last_key = 'static';
|
|
} elseif ( 'static' === $split_source[0] ) {
|
|
$last_key = 'static';
|
|
} else {
|
|
$last_key = array_pop( $split_source );
|
|
}
|
|
|
|
if ( array_key_exists( $last_key, $product_data ) && is_array( $product_data[ $last_key ] ) ) {
|
|
return $product_data[ $last_key ];
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected function meta_data_contains_category_data( $meta_data ) {
|
|
if ( ! property_exists( $meta_data, 'value' ) || empty( $meta_data->value ) ) {
|
|
return false;
|
|
}
|
|
|
|
$meta_obj = json_decode( $meta_data->value );
|
|
|
|
return property_exists( $meta_obj, 't' );
|
|
}
|
|
|
|
/**
|
|
* Returns an end value from a source that has an edit value input.
|
|
*
|
|
* @param array $change_parameters the change parameters including filter parameters if set.
|
|
* @param string $original_output the output before the change.
|
|
* @param array $product_data the product data.
|
|
* @param bool $combination_string true if the source is a combined string.
|
|
* @param string $feed_language the language of the feed.
|
|
* @param string $feed_currency the feed currency.
|
|
*
|
|
* @return string the end value.
|
|
*/
|
|
private function get_edited_end_row_value( $change_parameters, $original_output, $product_data, $combination_string, $feed_language, $feed_currency ) {
|
|
$result_is_filtered = false;
|
|
$support_class = new WPPFM_Feed_Support();
|
|
$final_output = '';
|
|
|
|
// Loop through the given change input rules.
|
|
for ( $i = 0; $i < count( $change_parameters ); $i ++ ) {
|
|
if ( property_exists( $change_parameters[ $i ], 'q' ) ) {
|
|
$filter_result = $this->filter_result( $change_parameters[ $i ]->q, $product_data );
|
|
} else {
|
|
$filter_result = true;
|
|
}
|
|
|
|
// Only change the value if the filter is true.
|
|
if ( true === $filter_result ) {
|
|
$combined_data_elements = $combination_string ? $this->get_combined_elements( $product_data, $combination_string ) : '';
|
|
$final_output = $support_class->edit_value(
|
|
$original_output,
|
|
$change_parameters[ $i ]->{$i + 1},
|
|
$combination_string,
|
|
$combined_data_elements,
|
|
$feed_language,
|
|
$feed_currency
|
|
);
|
|
|
|
$original_output = $final_output; // Set the new output as the original output for the next change rule.
|
|
$result_is_filtered = true;
|
|
}
|
|
}
|
|
|
|
// If the rules are all filtered out, the original output needs to be returned.
|
|
if ( false === $result_is_filtered ) {
|
|
$final_output = $original_output;
|
|
}
|
|
|
|
return $final_output;
|
|
}
|
|
|
|
/**
|
|
* Returns an array with the elements of a combined source selector.
|
|
*
|
|
* @param array $product_data the product data.
|
|
* @param string $combination_string the combination data.
|
|
*
|
|
* @return array with the combined elements.
|
|
*/
|
|
private function get_combined_elements( $product_data, $combination_string ) {
|
|
$result = array();
|
|
$found_all_data = true;
|
|
|
|
$combination_elements = explode( '|', $combination_string );
|
|
|
|
if ( false === strpos( $combination_elements[0], 'static#' ) ) {
|
|
if ( array_key_exists( $combination_elements[0], $product_data ) ) {
|
|
$result[] = $product_data[ $combination_elements[0] ];
|
|
} else {
|
|
$found_all_data = false;
|
|
}
|
|
} else {
|
|
$element = explode( '#', $combination_elements[0] );
|
|
$result[] = $element[1];
|
|
}
|
|
|
|
for ( $i = 1; $i <= count( $combination_elements ) - 1; $i ++ ) {
|
|
$pos = strpos( $combination_elements[ $i ], '#' );
|
|
$selector = substr( $combination_elements[ $i ], ( false !== $pos ? $pos + 1 : 0 ) );
|
|
|
|
if ( substr( $selector, 0, 7 ) === 'static#' ) {
|
|
$selector = explode( '#', $selector );
|
|
$result[] = $selector[1];
|
|
} elseif ( array_key_exists( $selector, $product_data ) ) {
|
|
$result[] = $product_data[ $selector ];
|
|
} else {
|
|
//array_push( $result, $selector );
|
|
$found_all_data = false;
|
|
}
|
|
}
|
|
|
|
if ( $found_all_data ) {
|
|
return $result;
|
|
} else {
|
|
$message = sprintf( 'Missing the data for one or both combined elements of the combination %s in the product with id %s.', $combination_string, $product_data['ID'] );
|
|
do_action( 'wppfm_feed_generation_message', $this->_feed_data->feedId, $message );
|
|
return array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a single string with the output from a combined source selection.
|
|
*
|
|
* @param array $source_selectors_array contains strings with the combined source selection elements.
|
|
* @param array $separators contains all possible separators for a combined source.
|
|
* @param array $product_data the product data.
|
|
* @param int|bool $array_pos the position in the array, if an array is selected as a combined element. False if this is not the case.
|
|
*
|
|
* @return string contains the combined output of a combined source selection.
|
|
*/
|
|
private function combine_the_outputs( $source_selectors_array, $separators, $product_data, $array_pos ) {
|
|
$combined_string = '';
|
|
|
|
foreach ( $source_selectors_array as $source ) {
|
|
$split_source = explode( '#', $source );
|
|
|
|
// Get the separator.
|
|
$separators_id = count( $split_source ) > 1 && 'static' !== $split_source[0] ? $split_source[0] : 0;
|
|
$sep = $separators[ $separators_id ];
|
|
|
|
$data_key = count( $split_source ) > 1 && 'static' !== $split_source[0] ? $split_source[1] : $split_source[0];
|
|
|
|
if ( ( array_key_exists( $data_key, $product_data ) && $product_data[ $data_key ] ) || 'static' === $data_key ) {
|
|
if ( 'static' !== $data_key && ! is_array( $product_data[ $data_key ] ) ) { // Not static and no array.
|
|
$combined_string .= $sep;
|
|
$combined_string .= $product_data[ $data_key ];
|
|
} elseif ( 'static' === $data_key ) { // Static inputs.
|
|
$static_string = count( $split_source ) > 2 ? $split_source[2] : $split_source[1];
|
|
$combined_string .= $sep . $static_string;
|
|
} else { // Array inputs.
|
|
$input_array = $product_data[ $data_key ][ $array_pos ];
|
|
$combined_string .= $sep . $input_array;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $combined_string;
|
|
}
|
|
|
|
/**
|
|
* Generates an array with the relations between the WooCommerce fields and the channel fields.
|
|
*
|
|
* @return array with the relations.
|
|
*/
|
|
public function channel_to_woocommerce_field_relations() {
|
|
$relations = array();
|
|
|
|
foreach ( $this->_feed->attributes as $attribute ) {
|
|
|
|
// Get the source name except for the category_mapping field.
|
|
if ( 'category_mapping' !== $attribute->fieldName ) {
|
|
$source = $this->get_source_from_attribute( $attribute );
|
|
}
|
|
|
|
if ( ! empty( $source ) ) {
|
|
// Correct Google product category source.
|
|
if ( 'google_product_category' === $attribute->fieldName ) {
|
|
$source = 'google_product_category';
|
|
}
|
|
|
|
// Correct Google identifier exists source.
|
|
if ( 'identifier_exists' === $attribute->fieldName ) {
|
|
$source = 'identifier_exists';
|
|
}
|
|
|
|
// Fill the relation array.
|
|
$a = array(
|
|
'field' => $attribute->fieldName,
|
|
'db' => $source,
|
|
);
|
|
$relations[] = $a;
|
|
}
|
|
}
|
|
|
|
if ( empty( $relations ) ) {
|
|
wppfm_write_log_file( 'Function get_channel_to_woocommerce_field_relations returned zero relations.' );
|
|
}
|
|
|
|
return $relations;
|
|
}
|
|
|
|
/**
|
|
* Extract the source name from the attribute string.
|
|
*
|
|
* @param object $attribute the attribute.
|
|
*
|
|
* @return string with the source.
|
|
*/
|
|
private function get_source_from_attribute( $attribute ) {
|
|
|
|
$value_source = property_exists( $attribute, 'value' ) ? $this->get_source_from_attribute_value( $attribute->value ) : '';
|
|
|
|
if ( ! empty( $value_source ) ) {
|
|
$source = $value_source;
|
|
} elseif ( property_exists( $attribute, 'advisedSource' ) && '' !== $attribute->advisedSource ) {
|
|
$source = $attribute->advisedSource;
|
|
} else {
|
|
$source = $attribute->fieldName;
|
|
}
|
|
|
|
return $source;
|
|
}
|
|
|
|
/**
|
|
* Extract the source value from the attribute string.
|
|
*
|
|
* @param string $value attribute string.
|
|
*
|
|
* @return string the source value.
|
|
*/
|
|
private function get_source_from_attribute_value( $value ) {
|
|
$source = '';
|
|
|
|
if ( $value ) {
|
|
$value_string = $this->get_source_string( $value );
|
|
|
|
$value_object = json_decode( $value_string );
|
|
|
|
if ( is_object( $value_object ) && property_exists( $value_object, 'source' ) ) {
|
|
$source = $value_object->source;
|
|
}
|
|
}
|
|
|
|
return $source;
|
|
}
|
|
|
|
/**
|
|
* Extracts the source string from a value string.
|
|
*
|
|
* @param string $value_string the value string.
|
|
*
|
|
* @return string the source string.
|
|
*/
|
|
private function get_source_string( $value_string ) {
|
|
$source_string = '';
|
|
|
|
if ( ! empty( $value_string ) ) {
|
|
$value_object = json_decode( $value_string );
|
|
|
|
if ( $value_object && is_object( $value_object ) && property_exists( $value_object, 'm' )
|
|
&& ! empty( $value_object->m[0] )
|
|
&& property_exists( $value_object->m[0], 's' ) ) {
|
|
$source_string = wp_json_encode( $value_object->m[0]->s );
|
|
}
|
|
}
|
|
|
|
return $source_string;
|
|
}
|
|
|
|
/**
|
|
* Generates an XML string of one product including its variations.
|
|
*
|
|
* @param array $product_placeholder contains the product data.
|
|
* @param string $category_name field name of the category.
|
|
* @param string $description_name field name of the description.
|
|
*
|
|
* @return string an XML string for the feed.
|
|
*/
|
|
protected function convert_data_to_xml( $product_placeholder, $category_name, $description_name, $channel ) {
|
|
return $product_placeholder ? $this->make_xml_string_row( $product_placeholder, $category_name, $description_name, $channel ) : '';
|
|
}
|
|
|
|
/**
|
|
* Generates an XML string for one product.
|
|
*
|
|
* @param array $product_placeholder contains all product data.
|
|
* @param string $category_name selected category name.
|
|
* @param string $description_name the name of the description.
|
|
* @param string $channel contains the channel id.
|
|
*
|
|
* @return string an XML string for the feed.
|
|
* @noinspection PhpUndefinedMethodInspection
|
|
*/
|
|
private function make_xml_string_row( $product_placeholder, $category_name, $description_name, $channel ) {
|
|
$product_node_name = function_exists( 'wppfm_product_node_name' ) ? wppfm_product_node_name( $channel ) : 'item';
|
|
$node_pre_tag_name = function_exists( 'wppfm_get_node_pre_tag' ) ? wppfm_get_node_pre_tag( $channel ) : 'g:';
|
|
$product_node = apply_filters( 'wppfm_xml_product_node_name', $product_node_name, $channel );
|
|
$node_pre_tag = apply_filters( 'wppfm_xml_product_pre_tag_name', $node_pre_tag_name, $channel );
|
|
// _channel_class functions are defined in the channel-specific Channel Class. But if that specific function does not exist, the function in this file will be used.
|
|
$attributes_with_sub_attributes = apply_filters( 'wppfm_attributes_with_sub_attributes', $this->_channel_class->keys_that_have_sub_tags() );
|
|
$attributes_repeated_fields = apply_filters( 'wppfm_attributes_that_are_repeatable', $this->_channel_class->keys_that_can_be_used_more_than_once() );
|
|
$sub_keys_for_subs_attributes = apply_filters( 'wppfm_keys_for_sub_attributes', $this->_channel_class->sub_keys_for_sub_tags() );
|
|
|
|
$this->_channel_class->add_xml_sub_tags( $product_placeholder, $sub_keys_for_subs_attributes, $attributes_with_sub_attributes, $node_pre_tag );
|
|
$xml_string = "<$product_node>";
|
|
|
|
// For each product value item.
|
|
foreach ( $product_placeholder as $key => $value ) {
|
|
if ( ! is_array( $value ) ) {
|
|
$xml_string .= $this->make_xml_string( $key, $value, $category_name, $description_name, $node_pre_tag, $attributes_with_sub_attributes, $attributes_repeated_fields );
|
|
} else {
|
|
$xml_string .= $this->make_xml_string_from_array( $key, $value, $node_pre_tag, $attributes_with_sub_attributes, $attributes_repeated_fields, $channel );
|
|
}
|
|
}
|
|
|
|
$xml_string .= "</$product_node>";
|
|
|
|
return $xml_string;
|
|
}
|
|
|
|
/**
|
|
* Generates a csv string of one product including its variations.
|
|
*
|
|
* @param array $product_placeholder contains the product data.
|
|
* @param array $active_fields contains all the active fields.
|
|
* @param string $csv_separator with the csv separator for this csv file.
|
|
*
|
|
* @return string a csv string for the feed.
|
|
*/
|
|
protected function convert_data_to_csv( $product_placeholder, $active_fields, $csv_separator ) {
|
|
if ( $product_placeholder ) {
|
|
if ( count( $product_placeholder ) > count( $active_fields ) ) {
|
|
$support_class = new WPPFM_Feed_Support();
|
|
$support_class->correct_active_fields_list( $active_fields );
|
|
}
|
|
|
|
// The first row in a csv file should contain the index, the following rows the data.
|
|
return $this->make_comma_separated_string_from_data_array( $product_placeholder, $active_fields, $this->_feed_data->channel, $csv_separator );
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a tab separated string for a tsv file.
|
|
*
|
|
* @param array $product_placeholder contains the product data.
|
|
*
|
|
* @return string a tsv string for the feed.
|
|
*/
|
|
protected function convert_data_to_tsv( $product_placeholder ) {
|
|
if ( $product_placeholder ) {
|
|
return $this->make_feed_string_from_product_placeholder( $product_placeholder, "\t" );
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a txt string for a txt file.
|
|
*
|
|
* @param array $product_placeholder contains the product data.
|
|
* @param string $separator the txt separator.
|
|
*
|
|
* @return string a txt string for the feed.
|
|
*/
|
|
protected function convert_data_to_txt( $product_placeholder, $separator ) {
|
|
if ( $product_placeholder ) {
|
|
return $this->make_feed_string_from_product_placeholder( $product_placeholder, $separator );
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Takes one row data and converts it to a tab delimited string.
|
|
*
|
|
* @param array $product_placeholder contains the product data.
|
|
* @param string $separator the separator.
|
|
*
|
|
* @return string with the feed string.
|
|
*/
|
|
protected function make_feed_string_from_product_placeholder( $product_placeholder, $separator ) {
|
|
$row_string = '';
|
|
|
|
foreach ( $product_placeholder as $row_item ) {
|
|
$a_row_item = ! is_array( $row_item ) ? preg_replace( "/[\r\n]/", "", $row_item ) : implode( ', ', $row_item );
|
|
$clean_row_item = wp_strip_all_tags( $a_row_item );
|
|
$row_string .= $clean_row_item;
|
|
|
|
'TAB' === $separator ? $row_string .= "\t" : $row_string .= $separator;
|
|
}
|
|
|
|
$row = 'TAB' === $separator ? trim( $row_string ) : trim( $row_string, $separator ); // removes the separator at the end of the line
|
|
|
|
return $row . "\r\n";
|
|
}
|
|
|
|
/**
|
|
* Takes the data for one row and converts it to a comma-separated string that fits into the feed.
|
|
*
|
|
* @param array $row_data Array with the attribute name => attribute data.
|
|
* @param array $active_fields Array containing the attributes that are active and need to go into the feed.
|
|
* @param string $channel Channel id.
|
|
* @param string $separator Requested data separator (default ,).
|
|
*
|
|
* @return string comma separated string with row data.
|
|
*/
|
|
private function make_comma_separated_string_from_data_array( $row_data, $active_fields, $channel, $separator = ',' ) {
|
|
$row_string = '';
|
|
|
|
$quotes_not_allowed = wppfm_channel_requires_no_quotes_on_empty_attributes( $channel );
|
|
|
|
// @since 2.11.0 allows choosing another separator for array data.
|
|
$separator_for_arrays = apply_filters( 'wppfm_separator_for_arrays_in_csv_feed', '|' );
|
|
|
|
// Loop through the active attributes.
|
|
foreach ( $active_fields as $row_item ) {
|
|
if ( array_key_exists( $row_item, $row_data ) ) {
|
|
$clean_row_item = ! is_array( $row_data[ $row_item ] ) ? preg_replace( "/[\r\n]/", '', $row_data[ $row_item ] ) : implode( $separator_for_arrays, $row_data[ $row_item ] );
|
|
} else {
|
|
$clean_row_item = '';
|
|
}
|
|
|
|
$quotes = $quotes_not_allowed && '' === $clean_row_item ? '' : '"';
|
|
|
|
$remove_double_quotes_from_string = str_replace( '"', "'", $clean_row_item );
|
|
$row_string .= $quotes . $remove_double_quotes_from_string . $quotes . $separator;
|
|
}
|
|
|
|
$row = rtrim( $row_string, $separator ); // Removes the comma at the end of the line.
|
|
|
|
return $row . "\r\n";
|
|
}
|
|
|
|
/**
|
|
* Generates the header string for a csv or tsv file.
|
|
*
|
|
* @param array $active_fields array with the active fields.
|
|
* @param string $separator the separator to use for the header.
|
|
*
|
|
* @return string with the header.
|
|
*/
|
|
protected function make_custom_header_string( $active_fields, $separator ) {
|
|
$header = implode( $separator, $active_fields );
|
|
|
|
return $header . "\r\n";
|
|
}
|
|
|
|
/**
|
|
* Make an array of product element strings.
|
|
*
|
|
* @param string $key the node id.
|
|
* @param array $value the array containing the values for the XML string.
|
|
* @param string $google_node_pre_tag the Google node tag to use.
|
|
* @param array $tags_with_sub_tags an array with attributes that have sub tags.
|
|
* @param array $tags_repeated_fields an array with attributes that can be used more than once.
|
|
* @param string $channel the channel id.
|
|
*
|
|
* @return string an XML string from an array of product elements.
|
|
* @noinspection PhpUndefinedMethodInspection
|
|
*/
|
|
private function make_xml_string_from_array( $key, $value, $google_node_pre_tag, $tags_with_sub_tags, $tags_repeated_fields, $channel ) {
|
|
$xml_strings = '';
|
|
|
|
for ( $i = 0; $i < count( $value ); $i ++ ) {
|
|
$xml_key = 'Extra_Afbeeldingen' === $key ? 'Extra_Image_' . ( $i + 1 ) : $key; // Required for Beslist.nl
|
|
$xml_strings .= $this->make_xml_string( $xml_key, $value[ $i ], '', '', $google_node_pre_tag, $tags_with_sub_tags, $tags_repeated_fields );
|
|
}
|
|
|
|
// Specific for the Atalanda channel option key.
|
|
if ( '38' === $channel && 'option' === $key ) {
|
|
$xml_strings = '<atalanda:options>' . str_replace( 'g:', 'atalanda:', $xml_strings ) . '</atalanda:options>';
|
|
}
|
|
|
|
return $xml_strings;
|
|
}
|
|
|
|
/**
|
|
* Generates an XML node.
|
|
*
|
|
* Returns an XML node for a product tag and uses the product data to make the node.
|
|
*
|
|
* @param string $key note id.
|
|
* @param string $xml_value note value.
|
|
* @param string $category_name category name.
|
|
* @param string $description_name description name.
|
|
* @param string $google_node_pre_tag pre node tag.
|
|
* @param array $tags_with_sub_tags array with tags that have a sub tag construction.
|
|
* @param array $tags_repeated_fields array with tags that are allowed to be placed in the feed more than once
|
|
*
|
|
* @since 1.1.0
|
|
* @since 2.34.0. Added a new line break at the end of each XML row to make a more readable XML feed and prevent large text lines.
|
|
* @return string Node string in xml format eg. <id>43</id>.
|
|
*/
|
|
private function make_xml_string( $key, $xml_value, $category_name, $description_name, $google_node_pre_tag, $tags_with_sub_tags, $tags_repeated_fields ) {
|
|
$xml_string = '';
|
|
$key = str_replace( ' ', '_', $key ); // @since 2.40.0
|
|
$repeated_field = in_array( $key, $tags_repeated_fields, true );
|
|
$subtag_sep = apply_filters( 'wppfm_sub_tag_separator', '||' );
|
|
|
|
if ( substr( $xml_value, 0, 5 ) === '!sub:' ) {
|
|
$sub_array = explode( '|', $xml_value );
|
|
$sa = $sub_array[0];
|
|
$st = explode( ':', $sa );
|
|
$sub_tag = $st[1];
|
|
$xml_value = "<$google_node_pre_tag$sub_tag>$sub_array[1]</$google_node_pre_tag$sub_tag>";
|
|
}
|
|
|
|
if ( $repeated_field && ! is_array( $xml_value ) ) {
|
|
$xml_value = explode( $subtag_sep, $xml_value );
|
|
}
|
|
|
|
// Keys to be added in a CDATA bracket to the XML feed.
|
|
$cdata_keys = apply_filters ( 'wppfm_cdata_keys', array(
|
|
$category_name,
|
|
$description_name,
|
|
'title'
|
|
) );
|
|
|
|
if ( ! is_array( $xml_value ) && ! in_array( $key, $tags_with_sub_tags, true ) ) {
|
|
if ( in_array( $key, $cdata_keys, true ) ) {
|
|
$xml_value = $this->convert_to_character_data_string( $xml_value ); // Put in a ![CDATA[...]] bracket.
|
|
} else {
|
|
$xml_value = $this->convert_to_xml_value( $xml_value );
|
|
}
|
|
}
|
|
|
|
if ( '' !== $key ) {
|
|
if ( is_array( $xml_value ) && $repeated_field ) {
|
|
foreach ( $xml_value as $value_item ) {
|
|
$xml_string .= $this->add_xml_string( $key, $value_item, $google_node_pre_tag );
|
|
}
|
|
} else {
|
|
$xml_string = $this->add_xml_string( $key, $xml_value, $google_node_pre_tag );
|
|
}
|
|
}
|
|
|
|
return $xml_string . "\r\n";
|
|
}
|
|
|
|
/**
|
|
* Generates a single XML line string.
|
|
*
|
|
* @param string $key the key to use.
|
|
* @param string $xml_value the value to use
|
|
* @param string $google_node_pre_tag the node pre tag to use.
|
|
*
|
|
* @since 1.9.0.
|
|
* @since 2.13.0 Added the wppfm_xml_element_attribute filter.
|
|
* @since 2.38.0 Removed a code part that would replace a - character by a _ character as the - character is not recommended for an XML file. But the Vivino XML channel requires the use of an _ in some of their keys.
|
|
* @since 3.8.0 Added the wppfm_xml_key_prefix_per_attribute filter to allow different XML key prefixes in one feed, different per attribute.
|
|
* @since 1.9.0
|
|
* @return string with a single XML line.
|
|
*/
|
|
private function add_xml_string( $key, $xml_value, $google_node_pre_tag ) {
|
|
//$not_allowed_characters = array( ' ', '-' );
|
|
//$clean_key = str_replace( $not_allowed_characters, '_', $key );
|
|
|
|
//@since 3.8.0
|
|
$google_node_pre_tag = apply_filters( 'wppfm_xml_key_prefix_per_attribute', $google_node_pre_tag, $key, $xml_value );
|
|
|
|
// @since 2.13.0
|
|
$element_attribute = apply_filters( 'wppfm_xml_element_attribute', '', $key, $xml_value );
|
|
$element_attribute_string = '' !== $element_attribute ? ' ' . $element_attribute : '';
|
|
|
|
//return "<$google_node_pre_tag$clean_key$element_attribute_string>$xml_value</$google_node_pre_tag$clean_key>";
|
|
return "<$google_node_pre_tag$key$element_attribute_string>$xml_value</$google_node_pre_tag$key>";
|
|
}
|
|
|
|
/**
|
|
* Converts an ordinary XML string into a CDATA string as long as it's not only a numeric value.
|
|
*
|
|
* @param string $string XML string to convert.
|
|
*
|
|
* @since 2.34.0. Added an utf8 check and rewritten the CDATA string rule.
|
|
* @since 3.5.0. Replaced the deprecated utf8_encode with mb_convert_encoding.
|
|
* @return string the CDATA string.
|
|
*/
|
|
protected function convert_to_character_data_string( $string ) {
|
|
if ( is_numeric( $string ) ) {
|
|
return $string;
|
|
}
|
|
|
|
if ( mb_detect_encoding( $string, 'UTF-8' ) ) { // string contains non-UTF-8 characters so they should be removed
|
|
$string = mb_convert_encoding( $string, 'UTF-8', mb_detect_encoding( $string ));
|
|
}
|
|
|
|
return '<![CDATA[' . str_replace( ']]>', ']]]]><![CDATA[>', $string ) . ']]>';
|
|
}
|
|
|
|
/**
|
|
* Can be overridden by a channel-specific function in its class-feed.php.
|
|
*
|
|
* @param $product_placeholder array Pointer to the product data.
|
|
* @param $sub_keys_for_subs array Array with the tags that can be placed in the feed as a sub tag (e.g. <loyalty_points><ratio>).
|
|
* @param $tags_repeated_fields array Array with tags of fields that can have more than one instance in the feed.
|
|
* @param $node_pre_tag string The channel dependant pre tag (eg. g: for Google Feeds).
|
|
*
|
|
* @since 1.9.0
|
|
* @since 2.37.0. Added the extra limit parameter to the explode function so that it can work with attributes that have an - in their name, like attributes used in the Vivino XML channel.
|
|
* @since 3.8.0. Added extra code that splits up sub-attributes if more than one sub-attribute is available in the $sub_tags variable.
|
|
* @return array The product with the correct XML tags.
|
|
*/
|
|
public function add_xml_sub_tags( &$product_placeholder, $sub_keys_for_subs, $tags_repeated_fields, $node_pre_tag ) {
|
|
$sub_tags = array_intersect_key( $product_placeholder, array_flip( $sub_keys_for_subs ) );
|
|
|
|
if ( count( $sub_tags ) < 1 ) {
|
|
return $product_placeholder;
|
|
}
|
|
|
|
$subtag_sep = apply_filters( 'wppfm_sub_tag_separator', '||' );
|
|
$tags_value = array();
|
|
$main_tag = '';
|
|
|
|
foreach ( $sub_tags as $key => $value ) {
|
|
$split = explode( '-', $key, 2 );
|
|
|
|
// @since 3.8.0
|
|
// For each main tag, the $tags_value needs to be reset in order to get a separate main element in the feed
|
|
$main_tag_changed = '' !== $main_tag && $main_tag !== $split[0];
|
|
if ( $main_tag_changed ) {
|
|
$tags_value = array();
|
|
}
|
|
|
|
$main_tag = $split[0];
|
|
|
|
if ( in_array( $split[0], $tags_repeated_fields, true ) ) {
|
|
$tags_counter = 0;
|
|
$value_array = is_array( $value ) ? $value : explode( $subtag_sep, $value );
|
|
|
|
foreach ( $value_array as $sub_value ) {
|
|
$prev_string = array_key_exists( $tags_counter, $tags_value ) ? $tags_value[ $tags_counter ] : '';
|
|
$tags_value[ $tags_counter ] = $prev_string . '<' . $node_pre_tag . $split[1] . '>' . $sub_value . '</' . $node_pre_tag . $split[1] . '>';
|
|
$tags_counter ++;
|
|
}
|
|
} else {
|
|
$tags_value = array_key_exists( $split[0], $product_placeholder ) ? $product_placeholder[ $split[0] ] : '';
|
|
$tags_value .= '<' . $node_pre_tag . $split[1] . '>' . $value . '</' . $node_pre_tag . $split[1] . '>';
|
|
}
|
|
|
|
unset( $product_placeholder[ $key ] );
|
|
$product_placeholder[ $split[0] ] = $tags_value;
|
|
}
|
|
|
|
return $product_placeholder;
|
|
}
|
|
|
|
/**
|
|
* Can be overridden by a channel-specific function in its class-feed.php. This version returns an empty array.
|
|
*
|
|
* @since 1.9.0
|
|
*
|
|
* @return array empty array.
|
|
*/
|
|
public function keys_that_have_sub_tags() {
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Can be overridden by a channel-specific function in its class-feed.php. This version returns and empty array.
|
|
*
|
|
* @since 2.1.0
|
|
*
|
|
* @return array empty array.
|
|
*/
|
|
public function sub_keys_for_sub_tags() {
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Can be overridden by a channel-specific function in its class-feed.php. this version returns an empty array.
|
|
*
|
|
* @since 1.9.0
|
|
*
|
|
* @return array empty array.
|
|
*/
|
|
public function keys_that_can_be_used_more_than_once() {
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Replaces certain characters to get a valid XML value.
|
|
*
|
|
* @param string $value_string the original value.
|
|
*
|
|
* @return string converted string.
|
|
*/
|
|
public function convert_to_xml_value( $value_string ) {
|
|
$string_without_tags = wp_strip_all_tags( $value_string );
|
|
$prep_string = str_replace(
|
|
array(
|
|
'&',
|
|
'<',
|
|
'>',
|
|
''',
|
|
'"',
|
|
' ',
|
|
),
|
|
array(
|
|
'&',
|
|
'<',
|
|
'>',
|
|
'\'',
|
|
'"',
|
|
'nbsp;',
|
|
),
|
|
$string_without_tags
|
|
);
|
|
|
|
return str_replace(
|
|
array(
|
|
'&',
|
|
'<',
|
|
'>',
|
|
'\'',
|
|
'"',
|
|
'nbsp;',
|
|
'`',
|
|
),
|
|
array(
|
|
'&',
|
|
'<',
|
|
'>',
|
|
''',
|
|
'"',
|
|
' ',
|
|
'',
|
|
),
|
|
$prep_string
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the translated attachment URL (full size) for a given attachment ID and language.
|
|
* Falls back to the original ID if no translation exists. Also normalizes protocol to https when needed.
|
|
*
|
|
* @param int $attachment_id Original attachment ID.
|
|
* @param string $selected_language Target language code.
|
|
*
|
|
* @since 3.16.0
|
|
* @return string Attachment URL or empty string.
|
|
*/
|
|
private function get_attachment_url_translated( $attachment_id, $selected_language ) {
|
|
if ( ! $attachment_id ) {
|
|
return '';
|
|
}
|
|
|
|
if ( has_filter( 'wpml_object_id' ) && is_plugin_active( 'wpml-media-translation/plugin.php' ) ) {
|
|
$translated_id = apply_filters( 'wpml_object_id', $attachment_id, 'attachment', true, $selected_language );
|
|
$attachment_id = $translated_id ? $translated_id : $attachment_id;
|
|
}
|
|
|
|
$url = wp_get_attachment_image_url( $attachment_id, 'full' );
|
|
|
|
if ( ! $url ) {
|
|
return '';
|
|
}
|
|
|
|
// Normalize to https if site is served over SSL.
|
|
if ( is_ssl() ) {
|
|
$url = str_replace( 'http://', 'https://', $url );
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* Get formal WooCommerce custom fields data.
|
|
*
|
|
* @param string $id the product id.
|
|
* @param string $parent_product_id the products parent id.
|
|
* @param string $field the field data.
|
|
*
|
|
* @since 2.0.9. added the $parent_product_id parameter.
|
|
* @return string with formal WooCommerce field data.
|
|
*/
|
|
protected function get_custom_field_data( $id, $parent_product_id, $field ) {
|
|
$custom_string = '';
|
|
$taxonomy = 'pa_' . $field;
|
|
$custom_values = get_the_terms( $id, $taxonomy );
|
|
|
|
if ( ! $custom_values && 0 !== $parent_product_id ) {
|
|
$custom_values = get_the_terms( $parent_product_id, $taxonomy );
|
|
}
|
|
|
|
if ( $custom_values ) {
|
|
foreach ( $custom_values as $custom_value ) {
|
|
$custom_string .= $custom_value->name . ', ';
|
|
}
|
|
}
|
|
|
|
return $custom_string ? substr( $custom_string, 0, - 2 ) : '';
|
|
}
|
|
|
|
/**
|
|
* Handles third party custom field data.
|
|
*
|
|
* @param string $product_id the product id.
|
|
* @param string $parent_product_id the products parent id.
|
|
* @param string $field the field data.
|
|
*
|
|
* @since 1.6.0
|
|
* @since 2.0.9. added the $parent_product_id parameter.
|
|
* @since 3.1.0. supports the ACF plugin.
|
|
* @return string with the correct field data.
|
|
*/
|
|
protected function get_third_party_custom_field_data( $product_id, $parent_product_id, $field ) {
|
|
$result = '';
|
|
$product_brand = '';
|
|
|
|
// YITH Brands plugin.
|
|
if ( get_option( 'yith_wcbr_brands_label' ) === $field ) { // YITH Brands plugin active
|
|
if ( has_term( '', 'yith_product_brand', $product_id ) ) {
|
|
$product_brand = get_the_terms( $product_id, 'yith_product_brand' );
|
|
}
|
|
|
|
if ( ! $product_brand && 0 !== $parent_product_id && has_term( '', 'yith_product_brand', $parent_product_id ) ) {
|
|
$product_brand = get_the_terms( $parent_product_id, 'yith_product_brand' );
|
|
}
|
|
|
|
if ( $product_brand && ! is_wp_error( $product_brand ) ) {
|
|
foreach ( $product_brand as $brand ) {
|
|
$result .= $brand->name . ', ';
|
|
}
|
|
}
|
|
}
|
|
|
|
// WooCommerce Brands plugin.
|
|
if ( in_array( 'woocommerce-brands/woocommerce-brands.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ), true ) ) {
|
|
|
|
if ( has_term( '', 'product_brand', $product_id ) ) {
|
|
$product_brand = get_the_terms( $product_id, 'product_brand' );
|
|
}
|
|
|
|
if ( ! $product_brand && 0 !== $parent_product_id && has_term( '', 'product_brand', $parent_product_id ) ) {
|
|
$product_brand = get_the_terms( $parent_product_id, 'product_brand' );
|
|
}
|
|
|
|
if ( $product_brand && ! is_wp_error( $product_brand ) ) {
|
|
foreach ( $product_brand as $brand ) {
|
|
$result .= $brand->name . ', ';
|
|
}
|
|
} elseif ( is_wp_error( $product_brand ) ) {
|
|
do_action( 'wppfm_feed_generation_warning', $product_id, $product_brand ); // @since 2.3.0
|
|
}
|
|
}
|
|
|
|
// Advanced Custom Fields (ACF) plugin.
|
|
// Handles the ACF custom fields.
|
|
// @since 3.1.0.
|
|
if ( function_exists( 'acf_get_field' ) && function_exists( 'acf_get_value' ) && function_exists( 'acf_format_value' ) ) {
|
|
$field_array = acf_get_field( $field );
|
|
|
|
if ( $field_array ) {
|
|
$field_value = acf_get_value( $product_id, $field_array );
|
|
$format_data = acf_format_value( $field_value, $product_id, $field_array );
|
|
|
|
// $field_array['type'] contains the Field Type
|
|
// $field_array['return_format'] contains the Return Format
|
|
|
|
if ( ! is_array( $format_data ) ) {
|
|
if ( $format_data instanceof WP_Term ) {
|
|
$format_data = $format_data->name;
|
|
}
|
|
|
|
// @since 3.4.0. handling the ACF True / False field type.
|
|
if ( is_bool( $format_data ) ) {
|
|
$format_data = $format_data ? 'true' : 'false';
|
|
}
|
|
|
|
$result = $format_data && is_string( $format_data ) ? $format_data . ', ' : '';
|
|
} else {
|
|
$data_string = '';
|
|
$separator = 'additional_image' === $field ? '||' : ', '; // for the additional_image attribute, use || as a separator
|
|
$separator = apply_filters( 'wppfm_acf_array_value_separator', $separator );
|
|
|
|
foreach ( $format_data as $data ) {
|
|
if ( $data instanceof WP_Term ) {
|
|
$data = $data->name;
|
|
}
|
|
|
|
$data_string .= $data && is_string( $data ) ? $data . $separator : '';
|
|
}
|
|
|
|
$result = $data_string;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result ? substr( $result, 0, - 2 ) : '';
|
|
}
|
|
|
|
/**
|
|
* Adds a single key with value to the active feed file.
|
|
*
|
|
* @param string $key the key.
|
|
* @param string $value the value.
|
|
*/
|
|
protected function write_single_general_xml_string_to_current_file( $key, $value ) {
|
|
$general_xml_string = sprintf( '<%s>%s</%s>', $key, $value, $key );
|
|
wppfm_append_line_to_file( $this->_feed_file_path, $general_xml_string, true );
|
|
}
|
|
|
|
/**
|
|
* Handles attributes that use their own procedures to ge the correct output value.
|
|
*
|
|
* @param object $product contains all the product data.
|
|
* @param WC_Product|null $woocommerce_product Optional. Already loaded WooCommerce product object to avoid redundant loading.
|
|
*
|
|
* @noinspection PhpPossiblePolymorphicInvocationInspection
|
|
*/
|
|
protected function handle_procedural_attributes( $product, $woocommerce_product = null ) {
|
|
// Use provided WC_Product object if available, otherwise load it
|
|
if ( null === $woocommerce_product ) {
|
|
$woocommerce_product = wc_get_product( $product->ID );
|
|
}
|
|
|
|
$feed_id = $this->_feed_data->feedId;
|
|
|
|
if ( false === $woocommerce_product ) {
|
|
$msg = sprintf( 'Failed to get the WooCommerce products procedural data from product %s.', $product->ID );
|
|
do_action( 'wppfm_feed_generation_warning', $feed_id, $msg ); // @since 2.3.0
|
|
return;
|
|
}
|
|
|
|
$active_field_names = $this->_pre_data['column_names'];
|
|
$selected_language = $this->_feed_data->language;
|
|
$selected_currency = $this->_feed_data->currency;
|
|
|
|
$woocommerce_parent_id = $woocommerce_product->get_parent_id();
|
|
$woocommerce_product_parent = $woocommerce_product->is_type( 'variable' ) || $woocommerce_product->is_type( 'variation' ) ? wc_get_product( $woocommerce_parent_id ) : null;
|
|
|
|
$price_context = get_option( 'wppfm_omit_price_filters', false ) ? 'view' : 'feed'; // $since 3.12.0.
|
|
|
|
if ( false === $woocommerce_product_parent || null === $woocommerce_product_parent ) {
|
|
// This product has no parent id, so it is possible this is the main of a variable product,
|
|
// so to make sure the general variation data like min_variation_price are available, copy the product
|
|
// in the parent product.
|
|
$woocommerce_product_parent = $woocommerce_product;
|
|
}
|
|
|
|
// @since 2.36.0.
|
|
if ( in_array( '_regular_price', $active_field_names, true ) ) {
|
|
if ( $woocommerce_product->is_type( 'variable' ) ) {
|
|
$product->_regular_price = wppfm_prep_money_values( $woocommerce_product->get_variation_regular_price( 'max', true ), $selected_language, $selected_currency );
|
|
} else {
|
|
$product->_regular_price = wppfm_prep_money_values( $woocommerce_product->get_regular_price( $price_context ), $selected_language, $selected_currency );
|
|
}
|
|
}
|
|
|
|
// @since 2.36.0.
|
|
// @since 2.40.0. Fixed the fact that the formal wc get_variation_sale_price function returns the regular price when no sale price is set for a variation.
|
|
if ( in_array( '_sale_price', $active_field_names, true ) ) {
|
|
if ( $woocommerce_product->is_type( 'variable' ) ) {
|
|
$product->_sale_price = wppfm_prep_money_values( $this->get_variation_sale_price( $woocommerce_product, 'max' ), $selected_language, $selected_currency );
|
|
} else {
|
|
$product->_sale_price = wppfm_prep_money_values( $woocommerce_product->get_sale_price( $price_context ), $selected_language, $selected_currency );
|
|
}
|
|
}
|
|
|
|
if ( in_array( 'shipping_class', $active_field_names, true ) ) {
|
|
// Get the shipping class.
|
|
$shipping_class = $woocommerce_product->get_shipping_class();
|
|
|
|
// If the shipping class in the product was empty and the product has a parent, then check if the parent has a shipping class.
|
|
$product->shipping_class = ! $shipping_class && $woocommerce_product_parent ? $woocommerce_product_parent->get_shipping_class() : $shipping_class;
|
|
}
|
|
|
|
if ( in_array( 'permalink', $active_field_names, true ) ) {
|
|
$permalink = get_permalink( $product->ID );
|
|
if ( false === $permalink && 0 !== $woocommerce_parent_id ) {
|
|
$permalink = get_permalink( $woocommerce_parent_id );
|
|
}
|
|
|
|
// WPML support.
|
|
$permalink = has_filter( 'wppfm_get_wpml_permalink' )
|
|
? apply_filters( 'wppfm_get_wpml_permalink', $permalink, $selected_language ) : $permalink;
|
|
|
|
// WOOCS support since @2.29.0.
|
|
$permalink = has_filter( 'wppfm_get_woocs_currency' )
|
|
? apply_filters( 'wppfm_woocs_product_permalink', $permalink, $selected_currency ) : $permalink;
|
|
|
|
// Translatepress support since @2.36.0.
|
|
$permalink = has_filter( 'wppfm_get_transpress_permalink' )
|
|
? apply_filters( 'wppfm_get_transpress_permalink', $permalink, $selected_language ) : $permalink;
|
|
|
|
// @since 3.7.0 - Add Google Analytics parameters to the product permalink.
|
|
// @since 3.11.0 - Added processing Google Analytics URL shortcodes.
|
|
if ( $this->_feed_data->google_analytics ) {
|
|
$permalink = $this->add_google_analytics_data_to_product_url( $permalink, $product->ID );
|
|
}
|
|
|
|
$product->permalink = $permalink;
|
|
}
|
|
|
|
// @since 3.16.0 - Removed the usage of the wppfm_get_wpml_permalink filter. Refactored the code to use the get_attachment_url_translated function.
|
|
if ( in_array( 'attachment_url', $active_field_names, true ) ) {
|
|
// Resolve the product's thumbnail URL (translated if applicable), fallback to parent.
|
|
$attachment_url = $this->get_attachment_url_translated( get_post_thumbnail_id( $product->ID ), $selected_language );
|
|
|
|
if ( ! $attachment_url && 0 !== $woocommerce_parent_id ) {
|
|
$attachment_url = $this->get_attachment_url_translated( get_post_thumbnail_id( $woocommerce_parent_id ), $selected_language );
|
|
}
|
|
|
|
$product->attachment_url = $attachment_url;
|
|
}
|
|
|
|
/**
|
|
* Source for the products main image, even for variable products it returns the image of the parent (main) product
|
|
* @since 3.4.0.
|
|
* @since 3.16.0 - Removed the usage of the wppfm_get_wpml_permalink filter. Refactored the code to use the get_attachment_url_translated function.
|
|
*/
|
|
if ( in_array( 'product_main_image_url', $active_field_names, true ) ) {
|
|
$main_product_id = 0 !== $woocommerce_parent_id ? $woocommerce_parent_id : $product->ID;
|
|
$main_image_url = $this->get_attachment_url_translated( get_post_thumbnail_id( $main_product_id ), $selected_language );
|
|
$product->product_main_image_url = $main_image_url;
|
|
}
|
|
|
|
if ( in_array( 'product_cat', $active_field_names, true ) ) {
|
|
$product->product_cat = WPPFM_Taxonomies::get_shop_categories( $product->ID );
|
|
if ( '' === $product->product_cat && 0 !== $woocommerce_parent_id ) {
|
|
$product->product_cat = WPPFM_Taxonomies::get_shop_categories( $woocommerce_parent_id );
|
|
}
|
|
}
|
|
|
|
if ( in_array( 'product_cat_string', $active_field_names, true ) ) {
|
|
$product->product_cat_string = WPPFM_Taxonomies::make_shop_taxonomies_string( $product->ID );
|
|
if ( '' === $product->product_cat_string && 0 !== $woocommerce_parent_id ) {
|
|
$product->product_cat_string = WPPFM_Taxonomies::make_shop_taxonomies_string( $woocommerce_parent_id );
|
|
}
|
|
}
|
|
|
|
if ( in_array( 'last_update', $active_field_names, true ) ) {
|
|
$product->last_update = gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) );
|
|
}
|
|
|
|
if ( in_array( '_wp_attachement_metadata', $active_field_names, true ) ) {
|
|
$product_id = 0 === $woocommerce_parent_id ? $product->ID : $woocommerce_parent_id;
|
|
$product->_wp_attachement_metadata = $this->get_product_image_gallery( $product_id, $selected_language );
|
|
}
|
|
|
|
if ( in_array( 'product_tags', $active_field_names, true ) ) {
|
|
// @since 2.41.0 - Corrected the code such that it also gives the tags of the parent.
|
|
$product_id = 0 === $woocommerce_parent_id ? $product->ID : $woocommerce_parent_id;
|
|
$product->product_tags = $this->get_product_tags( $product_id );
|
|
}
|
|
|
|
if ( in_array( 'wc_currency', $active_field_names, true ) ) {
|
|
// WPML support.
|
|
$product->wc_currency = has_filter( 'wppfm_get_translated_currency' )
|
|
? apply_filters( 'wppfm_get_translated_currency', get_woocommerce_currency(), $selected_language ) : get_woocommerce_currency();
|
|
}
|
|
|
|
if ( $woocommerce_product_parent && ( $woocommerce_product_parent->is_type( 'variable' ) || $woocommerce_product_parent->is_type( 'variation' ) ) ) {
|
|
if ( in_array( '_min_variation_price', $active_field_names, true ) ) {
|
|
$product->_min_variation_price = wppfm_prep_money_values( $woocommerce_product_parent->get_variation_price(), $selected_language, $selected_currency );
|
|
}
|
|
|
|
if ( in_array( '_max_variation_price', $active_field_names, true ) ) {
|
|
$product->_max_variation_price = wppfm_prep_money_values( $woocommerce_product_parent->get_variation_price( 'max' ), $selected_language, $selected_currency );
|
|
}
|
|
|
|
if ( in_array( '_min_variation_regular_price', $active_field_names, true ) ) {
|
|
$product->_min_variation_regular_price = wppfm_prep_money_values( $woocommerce_product_parent->get_variation_regular_price(), $selected_language, $selected_currency );
|
|
}
|
|
|
|
if ( in_array( '_max_variation_regular_price', $active_field_names, true ) ) {
|
|
$product->_max_variation_regular_price = wppfm_prep_money_values( $woocommerce_product_parent->get_variation_regular_price( 'max' ), $selected_language, $selected_currency );
|
|
}
|
|
|
|
// @since 2.40.0. - Fixed the fact that the formal wc get_variation_sale_price function returns the regular price when no sale price is set for a variation.
|
|
if ( in_array( '_min_variation_sale_price', $active_field_names, true ) ) {
|
|
$product->_min_variation_sale_price = wppfm_prep_money_values( $this->get_variation_sale_price( $woocommerce_product_parent ), $selected_language, $selected_currency );
|
|
}
|
|
|
|
// @since 2.40.0. - Fixed the fact that the formal wc get_variation_sale_price function returns the regular price when no sale price is set for a variation.
|
|
if ( in_array( '_max_variation_sale_price', $active_field_names, true ) ) {
|
|
$product->_max_variation_sale_price = wppfm_prep_money_values( $this->get_variation_sale_price( $woocommerce_product_parent, 'max' ), $selected_language, $selected_currency );
|
|
}
|
|
|
|
if ( in_array( 'item_group_id', $active_field_names, true ) ) {
|
|
$product->item_group_id = $this->get_item_group_id( $woocommerce_parent_id );
|
|
}
|
|
} else {
|
|
// @since 2.37.0. - Added code to handle min and max variation prices for non-variation products.
|
|
if ( in_array( '_min_variation_price', $active_field_names, true ) ) {
|
|
$product->_min_variation_price = wppfm_prep_money_values( $woocommerce_product->get_regular_price( $price_context ), $selected_language, $selected_currency );
|
|
}
|
|
|
|
if ( in_array( '_max_variation_price', $active_field_names, true ) ) {
|
|
$product->_max_variation_price = wppfm_prep_money_values( $woocommerce_product->get_regular_price( $price_context ), $selected_language, $selected_currency );
|
|
}
|
|
|
|
if ( in_array( '_min_variation_regular_price', $active_field_names, true ) ) {
|
|
$product->_min_variation_regular_price = wppfm_prep_money_values( $woocommerce_product->get_regular_price( $price_context ), $selected_language, $selected_currency );
|
|
}
|
|
|
|
if ( in_array( '_max_variation_regular_price', $active_field_names, true ) ) {
|
|
$product->_max_variation_regular_price = wppfm_prep_money_values( $woocommerce_product->get_regular_price( $price_context ), $selected_language, $selected_currency );
|
|
}
|
|
|
|
if ( in_array( '_min_variation_sale_price', $active_field_names, true ) ) {
|
|
$product->_min_variation_sale_price = wppfm_prep_money_values( $woocommerce_product->get_sale_price( $price_context ), $selected_language, $selected_currency );
|
|
}
|
|
|
|
if ( in_array( '_max_variation_sale_price', $active_field_names, true ) ) {
|
|
$product->_max_variation_sale_price = wppfm_prep_money_values( $woocommerce_product->get_sale_price( $price_context ), $selected_language, $selected_currency );
|
|
}
|
|
|
|
if ( ! $woocommerce_product_parent->is_type( 'simple' ) && ! $woocommerce_product_parent->is_type( 'grouped' )
|
|
&& ! $woocommerce_product_parent->is_type( 'virtual' ) && ! $woocommerce_product_parent->is_type( 'downloadable' )
|
|
&& ! $woocommerce_product_parent->is_type( 'external' ) ) {
|
|
$msg = sprintf(
|
|
'Product type of product %s could not be identified. The products shows as type %s',
|
|
$product->ID,
|
|
function_exists( 'get_product_type' ) ? get_product_type( $product->ID ) : 'unknown'
|
|
);
|
|
do_action( 'wppfm_feed_generation_warning', $feed_id, $msg ); // @since 2.3.0
|
|
}
|
|
}
|
|
|
|
if ( in_array( '_stock', $active_field_names, true ) ) {
|
|
$product->_stock = $woocommerce_product->get_stock_quantity();
|
|
}
|
|
|
|
// @since 2.1.4.
|
|
if ( in_array( 'empty', $active_field_names, true ) ) {
|
|
$product->empty = '';
|
|
}
|
|
|
|
// @since 2.2.0.
|
|
if ( in_array( 'product_type', $active_field_names, true ) ) {
|
|
$product->product_type = $woocommerce_product->get_type();
|
|
}
|
|
|
|
// @since 3.22.0. One path only: primary category breadcrumb, else first assigned term (by term_id).
|
|
if ( in_array( 'product_category_primary', $active_field_names, true ) ) {
|
|
$product->product_category_primary = WPPFM_Taxonomies::make_main_product_category_string( $product->ID );
|
|
if ( '' === $product->product_category_primary && 0 !== $woocommerce_parent_id ) {
|
|
$product->product_category_primary = WPPFM_Taxonomies::make_main_product_category_string( $woocommerce_parent_id );
|
|
}
|
|
}
|
|
|
|
// @since 2.2.0.
|
|
if ( in_array( 'product_variation_title_without_attributes', $active_field_names, true ) ) {
|
|
$product_title = get_post_field( 'post_title', $product->ID );
|
|
|
|
if ( false !== strpos( $product_title, ' - ' ) ) { // Assuming that the woocommerce_product_variation_title_attributes_separator is ' - '.
|
|
$title_parts = explode( ' - ', $product_title );
|
|
$product_title = $title_parts[0];
|
|
}
|
|
|
|
$product->product_variation_title_without_attributes = $product_title;
|
|
}
|
|
|
|
// @since 2.21.0.
|
|
if ( in_array( '_variation_parent_id', $active_field_names, true ) ) {
|
|
$product->_variation_parent_id = $woocommerce_parent_id;
|
|
}
|
|
|
|
// @since 2.21.0.
|
|
if ( in_array( '_product_parent_id', $active_field_names, true ) ) {
|
|
$product->_product_parent_id = $woocommerce_parent_id ?: '0';
|
|
}
|
|
|
|
// @since 2.21.0.
|
|
if ( in_array( '_max_group_price', $active_field_names, true ) && $woocommerce_product_parent->is_type( 'grouped' ) ) {
|
|
$product->_max_group_price = $this->get_group_price( $woocommerce_product_parent, 'max' );
|
|
}
|
|
|
|
// @since 2.21.0.
|
|
if ( in_array( '_min_group_price', $active_field_names, true ) && $woocommerce_product_parent->is_type( 'grouped' ) ) {
|
|
$product->_min_group_price = $this->get_group_price( $woocommerce_product_parent );
|
|
}
|
|
|
|
// @since 2.26.0.
|
|
// @since 2.36.0.- Changed the way the regular price is fetched for variation products.
|
|
if ( in_array( '_regular_price_with_tax', $active_field_names, true ) ) {
|
|
if ( $woocommerce_product->is_type( 'variable' ) ) {
|
|
$regular_price = $woocommerce_product->get_variation_regular_price( 'max' );
|
|
} else {
|
|
$regular_price = $woocommerce_product->get_regular_price( $price_context );
|
|
}
|
|
$feed_country_code = $this->_feed_data->country;
|
|
$localized = $this->get_localized_price_for_country( $woocommerce_product, $regular_price, $feed_country_code, $selected_currency );
|
|
$product->_regular_price_with_tax = wppfm_prep_money_values( $localized, $selected_language, $selected_currency );
|
|
}
|
|
|
|
// @since 2.26.0.
|
|
// @since 2.36.0.- Changed the way the regular price is fetched for variation products.
|
|
if ( in_array( '_regular_price_without_tax', $active_field_names, true ) ) {
|
|
if ( $woocommerce_product->is_type( 'variable' ) ) {
|
|
$regular_price = $woocommerce_product->get_variation_regular_price( 'max' );
|
|
} else {
|
|
$regular_price = $woocommerce_product->get_regular_price( $price_context );
|
|
}
|
|
$feed_country_code = $this->_feed_data->country;
|
|
$price = $this->get_localized_price_ex_tax_for_country( $woocommerce_product, $regular_price, $feed_country_code, $selected_currency );
|
|
if ( '' !== $price && $price !== null ) {
|
|
$product->_regular_price_without_tax = wppfm_prep_money_values( $price, $selected_language, $selected_currency );
|
|
}
|
|
}
|
|
|
|
// @since 2.26.0.
|
|
// @since 2.36.0.- Changed the way the sale price is fetched for variation products
|
|
// @since 2.37.0.- Added a check if the sale price is empty because the wc_get_price_including_tax function will return an unwanted regular price if the sale price is empty
|
|
// @since 2.40.0.- Fixed the fact that the formal wc get_variation_sale_price function returns the regular price when no sale price is set for a variation.
|
|
if ( in_array( '_sale_price_with_tax', $active_field_names, true ) ) {
|
|
if ( $woocommerce_product->is_type( 'variable' ) ) {
|
|
$sale_price = $this->get_variation_sale_price( $woocommerce_product, 'max' );
|
|
} else {
|
|
$sale_price = $woocommerce_product->get_sale_price( $price_context );
|
|
}
|
|
|
|
if ( $sale_price ) {
|
|
$feed_country_code = $this->_feed_data->country;
|
|
$localized = $this->get_localized_price_for_country( $woocommerce_product, $sale_price, $feed_country_code, $selected_currency );
|
|
$product->_sale_price_with_tax = wppfm_prep_money_values( $localized, $selected_language, $selected_currency );
|
|
}
|
|
}
|
|
|
|
// @since 2.26.0.
|
|
// @since 2.36.0.- Changed the way the sale price is fetched for variation products
|
|
// @since 2.37.0.- Added a check if the sale price is empty because the wc_get_price_including_tax function will return an unwanted regular price if the sale price is empty
|
|
// @since 2.40.0.- Fixed the fact that the formal wc get_variation_sale_price function returns the regular price when no sale price is set for a variation.
|
|
if ( in_array( '_sale_price_without_tax', $active_field_names, true ) ) {
|
|
if ( $woocommerce_product->is_type( 'variable' ) ) {
|
|
$sale_price = $this->get_variation_sale_price( $woocommerce_product, 'max' );
|
|
} else {
|
|
$sale_price = $woocommerce_product->get_sale_price( $price_context );
|
|
}
|
|
|
|
if ( $sale_price ) {
|
|
$feed_country_code = $this->_feed_data->country;
|
|
$price = $this->get_localized_price_ex_tax_for_country( $woocommerce_product, $sale_price, $feed_country_code, $selected_currency );
|
|
if ( '' !== $price && $price !== null ) {
|
|
$product->_sale_price_without_tax = wppfm_prep_money_values( $price, $selected_language, $selected_currency );
|
|
}
|
|
}
|
|
}
|
|
|
|
// @since 2.28.0.
|
|
if ( in_array( '_product_parent_description', $active_field_names, true ) ) {
|
|
$product->_product_parent_description = $woocommerce_product_parent->get_description( 'feed' );
|
|
}
|
|
|
|
// @since 2.28.0.
|
|
if ( in_array( '_woocs_currency', $active_field_names, true ) ) {
|
|
// WOOCS support
|
|
$product->_woocs_currency = has_filter( 'wppfm_get_woocs_currency' )
|
|
? apply_filters( 'wppfm_get_woocs_currency', $selected_currency ) : get_woocommerce_currency();
|
|
}
|
|
|
|
// @since 3.15.0.
|
|
if ( in_array( '_low_stock_amount', $active_field_names, true ) ) {
|
|
$low_stock_amount = wc_get_low_stock_amount( $woocommerce_product );
|
|
if ( $low_stock_amount ) {
|
|
$product->_low_stock_amount = $low_stock_amount;
|
|
}
|
|
}
|
|
|
|
// Performance metrics from feedmanager_product_performance (tier, revenue, orders).
|
|
// @since 3.21.0
|
|
if ( $this->performance_fields_requested( $active_field_names ) ) {
|
|
// When performance prioritizing is disabled for this feed, do not leak any previously computed data.
|
|
// Instead, resolve performance fields to empty values so they cannot appear in the generated feed.
|
|
if ( $this->is_performance_prioritizing_enabled_for_feed() ) {
|
|
$this->inject_performance_attributes( $product );
|
|
} else {
|
|
$this->clear_performance_attributes( $product );
|
|
}
|
|
}
|
|
|
|
$woocommerce_product = null;
|
|
}
|
|
|
|
/**
|
|
* Returns whether performance prioritizing is enabled for the current feed.
|
|
*
|
|
* This is cached per request to avoid repeated meta lookups during feed generation.
|
|
*
|
|
* @since 3.21.0
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function is_performance_prioritizing_enabled_for_feed() {
|
|
$feed_id = (int) ( $this->_feed_data->feedId ?? 0 );
|
|
if ( ! $feed_id ) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
null !== $this->_wppfm_performance_enabled_cache
|
|
&& (int) $this->_wppfm_performance_enabled_cache_feed_id === $feed_id
|
|
) {
|
|
return (bool) $this->_wppfm_performance_enabled_cache;
|
|
}
|
|
|
|
$queries = new WPPFM_Queries();
|
|
$meta = $queries->get_feed_performance_meta( $feed_id, array( 'wppfm_performance_enabled' ) );
|
|
$enabled = 'true' === ( $meta['wppfm_performance_enabled'] ?? 'false' );
|
|
|
|
$this->_wppfm_performance_enabled_cache = $enabled;
|
|
$this->_wppfm_performance_enabled_cache_feed_id = $feed_id;
|
|
|
|
return $enabled;
|
|
}
|
|
|
|
/**
|
|
* Clears performance attributes on the product object (ensures empty output).
|
|
*
|
|
* @since 3.21.0
|
|
*
|
|
* @param object $product Product data object (modified by reference).
|
|
*
|
|
* @return void
|
|
*/
|
|
private function clear_performance_attributes( $product ) {
|
|
$product->wppfm_performance_tier = '';
|
|
$product->wppfm_performance_revenue = '';
|
|
$product->wppfm_performance_orders = '';
|
|
}
|
|
|
|
/**
|
|
* Checks if any performance procedural fields are requested.
|
|
*
|
|
* @param array $active_field_names Column names requested for the feed.
|
|
*
|
|
* @return bool
|
|
* @since 3.21.0
|
|
*/
|
|
private function performance_fields_requested( $active_field_names ) {
|
|
$perf_fields = array( 'wppfm_performance_tier', 'wppfm_performance_revenue', 'wppfm_performance_orders' );
|
|
foreach ( $perf_fields as $field ) {
|
|
if ( in_array( $field, $active_field_names, true ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Injects performance tier, revenue, and orders into the product object.
|
|
*
|
|
* Looks up the product's row in feedmanager_product_performance for the current feed.
|
|
* The product ID used matches the feed queue (variation or parent per includeVariations).
|
|
* When no row exists, uses defaults: tier=low, revenue=0, orders=0.
|
|
*
|
|
* @param object $product Product data object (modified by reference).
|
|
*
|
|
* @since 3.21.0
|
|
*/
|
|
private function inject_performance_attributes( $product ) {
|
|
// Defensive guard: never output performance data when performance prioritizing is disabled.
|
|
if ( ! $this->is_performance_prioritizing_enabled_for_feed() ) {
|
|
$this->clear_performance_attributes( $product );
|
|
return;
|
|
}
|
|
|
|
$feed_id = $this->_feed_data->feedId ?? 0;
|
|
if ( ! $feed_id ) {
|
|
$this->clear_performance_attributes( $product );
|
|
return;
|
|
}
|
|
|
|
// Read from per-request prefetch cache when available (avoids N+1 queries per product).
|
|
if ( function_exists( 'wppfm_performance_cache' ) ) {
|
|
$cache = wppfm_performance_cache();
|
|
if ( is_array( $cache ) ) {
|
|
$row = isset( $cache[ (int) $product->ID ] ) ? $cache[ (int) $product->ID ] : null;
|
|
if ( $row ) {
|
|
$product->wppfm_performance_tier = in_array( (string) $row->performance_tier, array( 'high', 'mid', 'low' ), true ) ? (string) $row->performance_tier : 'low';
|
|
$product->wppfm_performance_revenue = is_numeric( $row->revenue ) ? number_format( (float) $row->revenue, 2, '.', '' ) : '0';
|
|
$product->wppfm_performance_orders = (string) (int) ( $row->orders_count ?? 0 );
|
|
} else {
|
|
$product->wppfm_performance_tier = 'low';
|
|
$product->wppfm_performance_revenue = '0';
|
|
$product->wppfm_performance_orders = '0';
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fallback: single lookup when cache not initialized (e.g. non-batch context).
|
|
$queries = new WPPFM_Queries();
|
|
$meta = $queries->get_feed_performance_meta( $feed_id, array( 'wppfm_performance_period_days' ) );
|
|
$period_days = (int) ( $meta['wppfm_performance_period_days'] ?? 30 );
|
|
$period_days = max( 7, min( 365, $period_days ) );
|
|
|
|
$row = $queries->get_performance_for_product( $feed_id, (int) $product->ID, $period_days );
|
|
|
|
if ( $row ) {
|
|
$product->wppfm_performance_tier = in_array( (string) $row->performance_tier, array( 'high', 'mid', 'low' ), true ) ? (string) $row->performance_tier : 'low';
|
|
$product->wppfm_performance_revenue = is_numeric( $row->revenue ) ? number_format( (float) $row->revenue, 2, '.', '' ) : '0';
|
|
$product->wppfm_performance_orders = (string) (int) ( $row->orders_count ?? 0 );
|
|
} else {
|
|
$product->wppfm_performance_tier = 'low';
|
|
$product->wppfm_performance_revenue = '0';
|
|
$product->wppfm_performance_orders = '0';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the item group id of a variation product.
|
|
*
|
|
* @param string $woocommerce_parent_id the parent product id.
|
|
*
|
|
* @since 3.11.0.
|
|
* @since 3.11.1 - To fix tickets #4302, #4311 and #4312, added a check if the parent product is a variation product.
|
|
* @return string the item group id.
|
|
*/
|
|
private function get_item_group_id( $woocommerce_parent_id ) {
|
|
$parent = wc_get_product( $woocommerce_parent_id );
|
|
|
|
// Ensure the product is either a variable product or variation.
|
|
if ( ! $parent || ! ( $parent->is_type( 'variable' ) || $parent->is_type( 'variation' ) ) ) {
|
|
return '';
|
|
}
|
|
|
|
$parent_sku = $parent->get_sku();
|
|
$product_id = $parent->get_id();
|
|
|
|
if ( $parent_sku ) {
|
|
return $parent_sku; // Best practise.
|
|
}
|
|
|
|
if ( $product_id ) {
|
|
return 'GID' . $product_id;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get all the gallery image urls.
|
|
*
|
|
* @param string $product_id the product id.
|
|
* @param string $selected_language selected language, if applicable.
|
|
*
|
|
* @since 3.16.0 - Removed the usage of the wppfm_get_wpml_permalink filter. Refactored the code to use the get_attachment_url_translated function.
|
|
* @return array|string an array with image urls or an empty string if none are found.
|
|
*/
|
|
private function get_product_image_gallery( $product_id, $selected_language ) {
|
|
$image_urls = array();
|
|
$images = 1;
|
|
$max_nr_images = 10;
|
|
|
|
$product = wc_get_product( $product_id );
|
|
$attachment_ids = $product->get_gallery_image_ids();
|
|
|
|
foreach ( $attachment_ids as $attachment_id ) {
|
|
$url = $this->get_attachment_url_translated( $attachment_id, $selected_language );
|
|
if ( $url ) {
|
|
$image_urls[] = $url;
|
|
$images ++;
|
|
}
|
|
|
|
if ( $images > $max_nr_images ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ! empty( $image_urls ) ? $image_urls : '';
|
|
}
|
|
|
|
/**
|
|
* Adds Google Analytics parameters to the product permalink.
|
|
*
|
|
* @param string $permalink the original permalink.
|
|
* @param string $product_id the product id.
|
|
*
|
|
* @since 3.11.0
|
|
* @return string modified permalink with Google Analytics parameters if they are added.
|
|
*/
|
|
private function add_google_analytics_data_to_product_url( $permalink, $product_id ) {
|
|
// Generate Google Analytics parameters with shortcodes replaced
|
|
$parameters = $this->get_google_analytics_parameters( $product_id );
|
|
|
|
// Add the parameters to the permalink
|
|
return add_query_arg( $parameters, $permalink );
|
|
}
|
|
|
|
/**
|
|
* Gets Google Analytics parameters for a product.
|
|
*
|
|
* @param string $product_id the product id.
|
|
*
|
|
* @since 3.11.0
|
|
* @return array Google Analytics parameters with shortcodes replaced.
|
|
*/
|
|
private function get_google_analytics_parameters( $product_id ) {
|
|
$google_analytics_data_holder = $this->_feed_data;
|
|
|
|
$parameters = array(
|
|
'utm_id' => $google_analytics_data_holder->utm_id,
|
|
'utm_source' => $google_analytics_data_holder->utm_source,
|
|
'utm_medium' => $google_analytics_data_holder->utm_medium,
|
|
'utm_campaign' => $google_analytics_data_holder->utm_campaign,
|
|
'utm_source_platform' => $google_analytics_data_holder->utm_source_platform,
|
|
'utm_term' => $google_analytics_data_holder->utm_term,
|
|
'utm_content' => $google_analytics_data_holder->utm_content,
|
|
);
|
|
|
|
// Replace shortcodes in each Google Analytics parameter value.
|
|
foreach ( $parameters as $key => $value ) {
|
|
if ( ! is_null( $value ) && $value !== '' ) {
|
|
$parameters[ $key ] = $this->replace_shortcodes( $value, $product_id );
|
|
}
|
|
}
|
|
|
|
// Filter out empty parameters.
|
|
return array_filter( $parameters, function( $value ) {
|
|
return ! is_null( $value ) && $value !== '';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Replaces shortcodes in a text with their actual values.
|
|
*
|
|
* @param string $text the text containing shortcodes.
|
|
* @param string $product_id the product id.
|
|
*
|
|
* @since 3.11.0
|
|
* @return string the text with shortcodes replaced.
|
|
*/
|
|
private function replace_shortcodes( $text, $product_id ) {
|
|
// Check for shortcodes in the text before proceeding.
|
|
if ( ! $this->has_shortcode( $text ) ) {
|
|
return $text; // No shortcodes found, return text as-is.
|
|
}
|
|
|
|
// Prepare only the shortcodes that are found in the text
|
|
$shortcodes = array(
|
|
'[product-id]' => strpos($text, '[product-id]') !== false ? $product_id : '',
|
|
'[product-sku]' => strpos($text, '[product-sku]') !== false ? get_post_meta( $product_id, '_sku', true ) : '',
|
|
'[product-title]' => strpos($text, '[product-title]') !== false ? get_the_title( $product_id ) : '',
|
|
'[product-group-id]' => strpos($text, '[product-group-id]') !== false ? $this->get_item_group_id( $product_id ) : '',
|
|
);
|
|
|
|
// Replace shortcodes in the text
|
|
foreach ( $shortcodes as $shortcode => $value ) {
|
|
$text = str_replace( $shortcode, $value, $text );
|
|
}
|
|
|
|
return $text;
|
|
}
|
|
|
|
/**
|
|
* Checks if a text contains any of the supported shortcodes.
|
|
*
|
|
* @param string $text the text to check.
|
|
*
|
|
* @since 3.11.0
|
|
* @return bool true if any of the shortcodes are found, false otherwise.
|
|
*/
|
|
private function has_shortcode( $text ) {
|
|
$shortcodes = array(
|
|
'[product-id]',
|
|
'[product-sku]',
|
|
'[product-title]',
|
|
'[product-group-id]',
|
|
);
|
|
|
|
foreach ( $shortcodes as $shortcode ) {
|
|
if ( strpos( $text, $shortcode ) !== false ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Gets the product tags.
|
|
*
|
|
* @param string $product_id the product id.
|
|
*
|
|
* @return string comma separated string containing the product tags.
|
|
*/
|
|
private function get_product_tags( $product_id ) {
|
|
$product_tags_string = '';
|
|
$product_tag_values = get_the_terms( $product_id, 'product_tag' );
|
|
$post_tag_values = get_the_tags( $product_id );
|
|
|
|
if ( $product_tag_values ) {
|
|
foreach ( $product_tag_values as $product_tag ) {
|
|
$product_tags_string .= $product_tag->name . ', ';
|
|
}
|
|
}
|
|
|
|
if ( $post_tag_values ) {
|
|
foreach ( $post_tag_values as $post_tag ) {
|
|
$product_tags_string .= $post_tag->name . ', ';
|
|
}
|
|
}
|
|
|
|
return $product_tags_string ? substr( $product_tags_string, 0, - 2 ) : '';
|
|
}
|
|
|
|
/**
|
|
* Returns the lowest or highest price of the products in a grouped product.
|
|
*
|
|
* @param WC_Product $woocommerce_product_parent grouped product data.
|
|
* @param string $min_max min (lowest) or max (highest) price, default min.
|
|
*
|
|
* @return string The highest or lowest price as a string.
|
|
*/
|
|
private function get_group_price( $woocommerce_product_parent, $min_max = 'min' ) {
|
|
$children = $woocommerce_product_parent->get_children();
|
|
$product_prices = array();
|
|
|
|
foreach ( $children as $value ) {
|
|
$product = wc_get_product( $value );
|
|
$product_prices[] = $product->get_price();
|
|
}
|
|
|
|
return 'min' === $min_max ? min( $product_prices ) : max( $product_prices );
|
|
}
|
|
|
|
/**
|
|
* Returns a localized product price converted to the feed currency and calculated with taxes for the feed country.
|
|
*
|
|
* This emulates the frontend price shown to a visitor from the target country, including product tax class rules.
|
|
*
|
|
* @param WC_Product $product WooCommerce product.
|
|
* @param float $raw_price Base price (regular/sale) before conversion.
|
|
* @param string $feed_country_code ISO country code for the feed target country (e.g. 'DE').
|
|
* @param string $feed_currency Target feed currency (e.g. 'EUR').
|
|
*
|
|
* @since 3.17.0
|
|
* @return float Localized gross price
|
|
*/
|
|
private function get_localized_price_for_country( $product, $raw_price, $feed_country_code, $feed_currency ) {
|
|
if ( '' === $raw_price || null === $raw_price ) {
|
|
return '';
|
|
}
|
|
// 1) Convert to feed currency via WPML Multi-currency when available
|
|
$converted_price = $raw_price;
|
|
$resolved_currency = $feed_currency ? $feed_currency : get_woocommerce_currency();
|
|
if ( defined( 'ICL_SITEPRESS_VERSION' ) ) {
|
|
$converted_price = apply_filters( 'wcml_raw_price_amount', $raw_price, $resolved_currency );
|
|
}
|
|
|
|
|
|
// 2) Calculate price including taxes while forcing the taxable address to the feed's country (no globals mutation).
|
|
$this->_temp_tax_country = $feed_country_code;
|
|
add_filter( 'woocommerce_customer_taxable_address', array( $this, 'override_tax_address' ), 999 );
|
|
try {
|
|
$localized_price = wc_get_price_including_tax( $product, array( 'price' => $converted_price ) );
|
|
} catch ( \Throwable $e ) {
|
|
$localized_price = (float) $converted_price; // fallback to converted price
|
|
}
|
|
remove_filter( 'woocommerce_customer_taxable_address', array( $this, 'override_tax_address' ), 999 );
|
|
$this->_temp_tax_country = null;
|
|
|
|
return apply_filters( 'wppfm_get_localized_product_price', $localized_price, $product, $feed_country_code, $feed_currency );
|
|
}
|
|
|
|
/**
|
|
* Returns a localized product price excluding taxes for the feed country, converted to the feed currency.
|
|
*
|
|
* When prices are entered inclusive of tax, WooCommerce requires the correct tax location to strip tax properly.
|
|
*
|
|
* @param WC_Product $product WooCommerce product.
|
|
* @param float $raw_price Base price (regular/sale) before conversion.
|
|
* @param string $feed_country_code ISO country code for the feed target country (e.g. 'DE').
|
|
* @param string $feed_currency Target feed currency (e.g. 'EUR').
|
|
*
|
|
* @since 3.17.0
|
|
* @return float Localized net price (excluding tax)
|
|
*/
|
|
private function get_localized_price_ex_tax_for_country( $product, $raw_price, $feed_country_code, $feed_currency ) {
|
|
// 1) Compute net price in store currency using WooCommerce tax logic and target country
|
|
if ( '' === $raw_price || null === $raw_price ) {
|
|
return '';
|
|
}
|
|
|
|
$this->_temp_tax_country = $feed_country_code;
|
|
add_filter( 'woocommerce_customer_taxable_address', array( $this, 'override_tax_address' ), 999 );
|
|
try {
|
|
$is_taxable = method_exists( $product, 'is_taxable' ) ? $product->is_taxable() : true;
|
|
$prices_include = function_exists( 'wc_prices_include_tax' ) ? wc_prices_include_tax() : false;
|
|
$tax_class = method_exists( $product, 'get_tax_class' ) ? $product->get_tax_class() : '';
|
|
$price_param = is_numeric( $raw_price ) ? (float) $raw_price : (float) $product->get_price();
|
|
|
|
// One-time explicit tax calculation to compute net if needed
|
|
$calc_total_tax = '';
|
|
$calc_taxes = array();
|
|
$sum_tax = 0.0;
|
|
if ( class_exists( 'WC_Tax' ) ) {
|
|
$rates_for_calc = WC_Tax::get_rates( $tax_class );
|
|
$calc_taxes = WC_Tax::calc_tax( (float) $price_param, $rates_for_calc, $prices_include );
|
|
$sum_tax = is_array( $calc_taxes ) ? array_sum( array_map( 'floatval', $calc_taxes ) ) : 0.0;
|
|
$calc_total_tax = sprintf( 'tax_sum:%s', $sum_tax );
|
|
}
|
|
|
|
// Two reference calculations
|
|
$net_no_arg = wc_get_price_excluding_tax( $product );
|
|
$net_with_arg = wc_get_price_excluding_tax( $product, array( 'price' => $price_param ) );
|
|
|
|
if ( $is_taxable && $prices_include ) {
|
|
// Prefer explicit inclusive-tax calculation when it yields a positive tax sum
|
|
if ( $sum_tax > 0 ) {
|
|
$net_by_calc = max( 0.0, (float) $price_param - (float) $sum_tax );
|
|
$net_store_currency = $net_by_calc;
|
|
} else {
|
|
// Fallback: choose the lowest positive candidate from WooCommerce helpers
|
|
$candidates = array_filter( array( (float) $net_no_arg, (float) $net_with_arg ), function( $v ) { return $v > 0; } );
|
|
$net_store_currency = ! empty( $candidates ) ? min( $candidates ) : (float) $price_param;
|
|
}
|
|
} else {
|
|
$net_store_currency = $net_no_arg;
|
|
}
|
|
} catch ( \Throwable $e ) {
|
|
$net_store_currency = (float) $raw_price; // fallback
|
|
}
|
|
remove_filter( 'woocommerce_customer_taxable_address', array( $this, 'override_tax_address' ), 999 );
|
|
$this->_temp_tax_country = null;
|
|
|
|
// 2) Convert net to feed currency via WPML (if active) using a scoped client currency override
|
|
$resolved_currency = $feed_currency ? $feed_currency : get_woocommerce_currency();
|
|
|
|
$this->_temp_currency = $resolved_currency;
|
|
if ( defined( 'ICL_SITEPRESS_VERSION' ) ) {
|
|
add_filter( 'wcml_client_currency', array( $this, 'override_client_currency' ), 999 );
|
|
}
|
|
|
|
$net_converted = defined( 'ICL_SITEPRESS_VERSION' )
|
|
? apply_filters( 'wcml_raw_price_amount', $net_store_currency, $resolved_currency )
|
|
: $net_store_currency;
|
|
|
|
if ( defined( 'ICL_SITEPRESS_VERSION' ) ) {
|
|
remove_filter( 'wcml_client_currency', array( $this, 'override_client_currency' ), 999 );
|
|
}
|
|
$this->_temp_currency = null;
|
|
|
|
return apply_filters( 'wppfm_get_localized_product_price_ex_tax', $net_converted, $product, $feed_country_code, $resolved_currency );
|
|
}
|
|
|
|
/**
|
|
* Forces the taxable address to the feed country while calculating prices.
|
|
*
|
|
* @param array $address [ country, state, postcode, city ]
|
|
* @return array
|
|
*/
|
|
public function override_tax_address( $address ) {
|
|
if ( $this->_temp_tax_country ) {
|
|
return array( $this->_temp_tax_country, '', '', '' );
|
|
}
|
|
return $address;
|
|
}
|
|
|
|
/**
|
|
* Forces the client currency for WPML Multi-currency while calculating prices.
|
|
*
|
|
* @param string $currency
|
|
* @return string
|
|
*/
|
|
public function override_client_currency( $currency ) {
|
|
return $this->_temp_currency ? $this->_temp_currency : $currency;
|
|
}
|
|
|
|
/**
|
|
* Fixes the fact that the formal wc get_variation_sale_price function returns the regular price when no sale price is set for a variation.
|
|
*
|
|
* @param object $wc_product the product data.
|
|
* @param string $min_or_max min (lowest) or max (highest) price, default min.
|
|
*
|
|
* @since 2.40.0
|
|
* @return mixed|string
|
|
*/
|
|
private function get_variation_sale_price( $wc_product, $min_or_max = 'min' ) {
|
|
$variation_sale_price = $wc_product->get_variation_sale_price( $min_or_max );
|
|
return $variation_sale_price < $wc_product->get_variation_regular_price( $min_or_max ) ? $variation_sale_price : '';
|
|
}
|
|
}
|