Merge pull request #26260 from woocommerce/fix/25524

Fix the visibility of partially out of stock variable products when using the layered nav widget
This commit is contained in:
Néstor Soriano 2020-07-28 09:54:09 +02:00 committed by GitHub
commit df94b6570d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1677 additions and 199 deletions

View File

@ -1501,6 +1501,16 @@ class WC_Product extends WC_Abstract_Legacy_Product {
* @return bool
*/
public function is_visible() {
$visible = $this->is_visible_core();
return apply_filters( 'woocommerce_product_is_visible', $visible, $this->get_id() );
}
/**
* Returns whether or not the product is visible in the catalog (doesn't trigger filters).
*
* @return bool
*/
protected function is_visible_core() {
$visible = 'visible' === $this->get_catalog_visibility() || ( is_search() && 'search' === $this->get_catalog_visibility() ) || ( ! is_search() && 'catalog' === $this->get_catalog_visibility() );
if ( 'trash' === $this->get_status() ) {
@ -1521,7 +1531,7 @@ class WC_Product extends WC_Abstract_Legacy_Product {
$visible = false;
}
return apply_filters( 'woocommerce_product_is_visible', $visible, $this->get_id() );
return $visible;
}
/**

View File

@ -149,6 +149,10 @@ class WC_Install {
'wc_update_400_reset_action_scheduler_migration_status',
'wc_update_400_db_version',
),
'4.4.0' => array(
'wc_update_440_insert_attribute_terms_for_variable_products',
'wc_update_440_db_version',
),
);
/**
@ -752,14 +756,14 @@ class WC_Install {
AND CONSTRAINT_NAME = 'fk_{$wpdb->prefix}wc_download_log_permission_id'
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'"
); // WPCS: unprepared SQL ok.
);
if ( 0 === (int) $fk_result->fk_count ) {
$wpdb->query(
"ALTER TABLE `{$wpdb->prefix}wc_download_log`
ADD CONSTRAINT `fk_{$wpdb->prefix}wc_download_log_permission_id`
FOREIGN KEY (`permission_id`)
REFERENCES `{$wpdb->prefix}woocommerce_downloadable_product_permissions` (`permission_id`) ON DELETE CASCADE;"
); // WPCS: unprepared SQL ok.
);
}
}

View File

@ -283,10 +283,11 @@ class WC_Product_Variable extends WC_Product {
* Get an array of available variations for the current product.
*
* @param bool $render_variations Prepares a data array for each variant for output in the add to cart form. Pass `false` to only return the available variations as objects.
* @return array
* @param bool $return_array_of_data If true, return an array of data for the variation; if false, return a WC_Product_Variation object.
*
* @return array|WC_Product_Variation
*/
public function get_available_variations( $render_variations = true ) {
public function get_available_variations( $render_variations = true, $return_array_of_data = true ) {
$variation_ids = $this->get_children();
$available_variations = array();
@ -309,7 +310,7 @@ class WC_Product_Variable extends WC_Product {
}
if ( $render_variations ) {
$available_variations[] = $this->get_available_variation( $variation );
$available_variations[] = $return_array_of_data ? $this->get_available_variation( $variation ) : $variation;
} else {
$available_variations[] = $variation;
}
@ -322,6 +323,27 @@ class WC_Product_Variable extends WC_Product {
return $available_variations;
}
/**
* Check if a given variation is currently available.
*
* @param WC_Product_Variation $variation Variation to check.
*
* @return bool True if the variation is available, false otherwise.
*/
private function variation_is_available( WC_Product_Variation $variation ) {
// Hide out of stock variations if 'Hide out of stock items from the catalog' is checked.
if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) {
return false;
}
// Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price).
if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) {
return false;
}
return true;
}
/**
* Returns an array of data for a variation. Used in the add to cart form.
*
@ -572,6 +594,95 @@ class WC_Product_Variable extends WC_Product {
return true;
}
/**
* Returns whether or not the product is visible in the catalog (doesn't trigger filters).
*
* @return bool
*/
protected function is_visible_core() {
if ( ! $this->parent_is_visible_core() ) {
return false;
}
$query_filters = $this->get_layered_nav_chosen_attributes();
if ( empty( $query_filters ) ) {
return true;
}
/**
* If there are attribute filters in the request, a variable product will be visible
* if at least one of the available variations matches the filters.
*/
$attributes_with_terms = array();
array_walk(
$query_filters,
function( $value, $key ) use ( &$attributes_with_terms ) {
$attributes_with_terms[ $key ] = $value['terms'];
}
);
$variations = $this->get_available_variations( true, false );
foreach ( $variations as $variation ) {
if ( $this->variation_matches_filters( $variation, $attributes_with_terms ) ) {
return true;
}
}
return false;
}
/**
* Checks if a given variation matches the active attribute filters.
*
* @param WC_Product_Variation $variation The variation to check.
* @param array $query_filters The active filters as an array of attribute_name => [term1, term2...].
*
* @return bool True if the variation matches the active attribute filters.
*/
private function variation_matches_filters( WC_Product_Variation $variation, array $query_filters ) {
// Get the variation attributes as an array of attribute_name => attribute_value.
// The array_filter will filter out attributes having a value of '', these correspond
// to "Any..." variations that don't participate in filtering.
$variation_attributes = array_filter( $variation->get_variation_attributes( false ) );
$variation_attribute_names_in_filters = array_intersect( array_keys( $query_filters ), array_keys( $variation_attributes ) );
if ( empty( $variation_attribute_names_in_filters ) ) {
// The variation doesn't have any attribute that participates in filtering so we consider it a match.
return true;
}
foreach ( $variation_attribute_names_in_filters as $attribute_name ) {
if ( ! in_array( $variation_attributes[ $attribute_name ], $query_filters[ $attribute_name ], true ) ) {
// Multiple filters interact with AND logic, so as soon as one of them
// doesn't match then the variation doesn't match.
return false;
}
}
return true;
}
/**
* What does is_visible_core in the parent class say?
* This method exists to ease unit testing.
*
* @return bool
*/
protected function parent_is_visible_core() {
return parent::is_visible_core();
}
/**
* Get an array of attributes and terms selected with the layered nav widget.
* This method exists to ease unit testing.
*
* @return array
*/
protected function get_layered_nav_chosen_attributes() {
return WC()->query::get_layered_nav_chosen_attributes();
}
/*
|--------------------------------------------------------------------------
| Sync with child variations.

View File

@ -110,15 +110,18 @@ class WC_Product_Variation extends WC_Product_Simple {
}
/**
* Get variation attribute values. Keys are prefixed with attribute_, as stored.
* Get variation attribute values. Keys are prefixed with attribute_, as stored, unless $with_prefix is false.
*
* @return array of attributes and their values for this variation
* @param bool $with_prefix Whether keys should be prepended with attribute_ or not, default is true.
* @return array of attributes and their values for this variation.
*/
public function get_variation_attributes() {
public function get_variation_attributes( $with_prefix = true ) {
$attributes = $this->get_attributes();
$variation_attributes = array();
$prefix = $with_prefix ? 'attribute_' : '';
foreach ( $attributes as $key => $value ) {
$variation_attributes[ 'attribute_' . $key ] = $value;
$variation_attributes[ $prefix . $key ] = $value;
}
return $variation_attributes;
}
@ -580,4 +583,22 @@ class WC_Product_Variation extends WC_Product_Simple {
return $valid_classes;
}
/**
* Delete variation, set the ID to 0, and return result.
*
* @since 4.4.0
* @param bool $force_delete Should the variation be deleted permanently.
* @return bool result
*/
public function delete( $force_delete = false ) {
$variation_id = $this->get_id();
if ( ! parent::delete( $force_delete ) ) {
return false;
}
wp_delete_object_term_relationships( $variation_id, wc_get_attribute_taxonomy_names() );
return true;
}
}

View File

@ -32,7 +32,7 @@ class WC_Query {
*
* @var array
*/
private static $_chosen_attributes;
private static $chosen_attributes;
/**
* Constructor for the query class. Hooks in methods.
@ -45,6 +45,7 @@ class WC_Query {
add_action( 'parse_request', array( $this, 'parse_request' ), 0 );
add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) );
add_filter( 'the_posts', array( $this, 'remove_product_query_filters' ) );
add_filter( 'found_posts', array( $this, 'adjust_posts_count' ) );
add_filter( 'get_pagenum_link', array( $this, 'remove_add_to_cart_pagination' ), 10, 1 );
}
$this->init_query_vars();
@ -54,7 +55,8 @@ class WC_Query {
* Get any errors from querystring.
*/
public function get_errors() {
$error = ! empty( $_GET['wc_error'] ) ? sanitize_text_field( wp_unslash( $_GET['wc_error'] ) ) : ''; // WPCS: input var ok, CSRF ok.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$error = ! empty( $_GET['wc_error'] ) ? sanitize_text_field( wp_unslash( $_GET['wc_error'] ) ) : '';
if ( $error && ! wc_has_notice( $error, 'error' ) ) {
wc_add_notice( $error, 'error' );
@ -217,14 +219,16 @@ class WC_Query {
public function parse_request() {
global $wp;
// phpcs:disable WordPress.Security.NonceVerification.Recommended
// Map query vars to their keys, or get them if endpoints are not supported.
foreach ( $this->get_query_vars() as $key => $var ) {
if ( isset( $_GET[ $var ] ) ) { // WPCS: input var ok, CSRF ok.
$wp->query_vars[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $var ] ) ); // WPCS: input var ok, CSRF ok.
if ( isset( $_GET[ $var ] ) ) {
$wp->query_vars[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $var ] ) );
} elseif ( isset( $wp->query_vars[ $var ] ) ) {
$wp->query_vars[ $key ] = $wp->query_vars[ $var ];
}
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
/**
@ -363,6 +367,61 @@ class WC_Query {
return $posts;
}
/**
* When the request is filtering by attributes via layered nav plugin we need to adjust the total posts count
* to account for variable products having stock in some variations but not in others.
* We do that by just checking if each product is visible.
*
* We also cache the post visibility so that it isn't checked again when displaying the posts list.
*
* @since 4.4.0
* @param int $count Original posts count, as supplied by the found_posts filter.
*
* @return int Adjusted posts count.
*/
public function adjust_posts_count( $count ) {
$posts = $this->get_current_posts();
if ( is_null( $posts ) ) {
return $count;
}
$count = 0;
foreach ( $posts as $post ) {
$id = is_object( $post ) ? $post->ID : $post;
$product = wc_get_product( $id );
if ( ! is_object( $product ) ) {
continue;
}
if ( $product->is_visible() ) {
wc_set_loop_product_visibility( $id, true );
$count++;
} else {
wc_set_loop_product_visibility( $id, false );
}
}
wc_set_loop_prop( 'total', $count );
return $count;
}
/**
* Instance version of get_layered_nav_chosen_attributes, needed for unit tests.
*
* @return array
*/
protected function get_layered_nav_chosen_attributes_inst() {
return self::get_layered_nav_chosen_attributes();
}
/**
* Get the posts (or the ids of the posts) found in the current WP loop.
*
* @return array Array of posts or post ids.
*/
protected function get_current_posts() {
return $GLOBALS['wp_query']->posts;
}
/**
* WP SEO meta description.
*
@ -447,7 +506,8 @@ class WC_Query {
public function get_catalog_ordering_args( $orderby = '', $order = '' ) {
// Get ordering from query string unless defined.
if ( ! $orderby ) {
$orderby_value = isset( $_GET['orderby'] ) ? wc_clean( (string) wp_unslash( $_GET['orderby'] ) ) : wc_clean( get_query_var( 'orderby' ) ); // WPCS: sanitization ok, input var ok, CSRF ok.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$orderby_value = isset( $_GET['orderby'] ) ? wc_clean( (string) wp_unslash( $_GET['orderby'] ) ) : wc_clean( get_query_var( 'orderby' ) );
if ( ! $orderby_value ) {
if ( is_search() ) {
@ -522,12 +582,15 @@ class WC_Query {
public function price_filter_post_clauses( $args, $wp_query ) {
global $wpdb;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! $wp_query->is_main_query() || ( ! isset( $_GET['max_price'] ) && ! isset( $_GET['min_price'] ) ) ) {
return $args;
}
$current_min_price = isset( $_GET['min_price'] ) ? floatval( wp_unslash( $_GET['min_price'] ) ) : 0; // WPCS: input var ok, CSRF ok.
$current_max_price = isset( $_GET['max_price'] ) ? floatval( wp_unslash( $_GET['max_price'] ) ) : PHP_INT_MAX; // WPCS: input var ok, CSRF ok.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$current_min_price = isset( $_GET['min_price'] ) ? floatval( wp_unslash( $_GET['min_price'] ) ) : 0;
$current_max_price = isset( $_GET['max_price'] ) ? floatval( wp_unslash( $_GET['max_price'] ) ) : PHP_INT_MAX;
// phpcs:enable WordPress.Security.NonceVerification.Recommended
/**
* Adjust if the store taxes are not displayed how they are stored.
@ -666,9 +729,11 @@ class WC_Query {
$product_visibility_not_in[] = $product_visibility_terms['outofstock'];
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended
// Filter by rating.
if ( isset( $_GET['rating_filter'] ) ) { // WPCS: input var ok, CSRF ok.
$rating_filter = array_filter( array_map( 'absint', explode( ',', $_GET['rating_filter'] ) ) ); // WPCS: input var ok, CSRF ok, Sanitization ok.
if ( isset( $_GET['rating_filter'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$rating_filter = array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) );
$rating_terms = array();
for ( $i = 1; $i <= 5; $i ++ ) {
if ( in_array( $i, $rating_filter, true ) && isset( $product_visibility_terms[ 'rated-' . $i ] ) ) {
@ -685,6 +750,7 @@ class WC_Query {
);
}
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( ! empty( $product_visibility_not_in ) ) {
$tax_query[] = array(
@ -753,8 +819,9 @@ class WC_Query {
$term = substr( $term, 1 );
}
$like = '%' . $wpdb->esc_like( $term ) . '%';
$sql[] = $wpdb->prepare( "(($wpdb->posts.post_title $like_op %s) $andor_op ($wpdb->posts.post_excerpt $like_op %s) $andor_op ($wpdb->posts.post_content $like_op %s))", $like, $like, $like ); // unprepared SQL ok.
$like = '%' . $wpdb->esc_like( $term ) . '%';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql[] = $wpdb->prepare( "(($wpdb->posts.post_title $like_op %s) $andor_op ($wpdb->posts.post_excerpt $like_op %s) $andor_op ($wpdb->posts.post_content $like_op %s))", $like, $like, $like );
}
if ( ! empty( $sql ) && ! is_user_logged_in() ) {
@ -770,11 +837,12 @@ class WC_Query {
* @return array
*/
public static function get_layered_nav_chosen_attributes() {
if ( ! is_array( self::$_chosen_attributes ) ) {
self::$_chosen_attributes = array();
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( ! is_array( self::$chosen_attributes ) ) {
self::$chosen_attributes = array();
if ( ! empty( $_GET ) ) { // WPCS: input var ok, CSRF ok.
foreach ( $_GET as $key => $value ) { // WPCS: input var ok, CSRF ok.
if ( ! empty( $_GET ) ) {
foreach ( $_GET as $key => $value ) {
if ( 0 === strpos( $key, 'filter_' ) ) {
$attribute = wc_sanitize_taxonomy_name( str_replace( 'filter_', '', $key ) );
$taxonomy = wc_attribute_taxonomy_name( $attribute );
@ -784,14 +852,15 @@ class WC_Query {
continue;
}
$query_type = ! empty( $_GET[ 'query_type_' . $attribute ] ) && in_array( $_GET[ 'query_type_' . $attribute ], array( 'and', 'or' ), true ) ? wc_clean( wp_unslash( $_GET[ 'query_type_' . $attribute ] ) ) : ''; // WPCS: sanitization ok, input var ok, CSRF ok.
self::$_chosen_attributes[ $taxonomy ]['terms'] = array_map( 'sanitize_title', $filter_terms ); // Ensures correct encoding.
self::$_chosen_attributes[ $taxonomy ]['query_type'] = $query_type ? $query_type : apply_filters( 'woocommerce_layered_nav_default_query_type', 'and' );
$query_type = ! empty( $_GET[ 'query_type_' . $attribute ] ) && in_array( $_GET[ 'query_type_' . $attribute ], array( 'and', 'or' ), true ) ? wc_clean( wp_unslash( $_GET[ 'query_type_' . $attribute ] ) ) : '';
self::$chosen_attributes[ $taxonomy ]['terms'] = array_map( 'sanitize_title', $filter_terms ); // Ensures correct encoding.
self::$chosen_attributes[ $taxonomy ]['query_type'] = $query_type ? $query_type : apply_filters( 'woocommerce_layered_nav_default_query_type', 'and' );
}
}
}
}
return self::$_chosen_attributes;
return self::$chosen_attributes;
// phpcs:disable WordPress.Security.NonceVerification.Recommended
}
/**
@ -804,7 +873,6 @@ class WC_Query {
return remove_query_arg( 'add-to-cart', $url );
}
// @codingStandardsIgnoreStart
/**
* Return a meta query for filtering by rating.
*
@ -819,7 +887,7 @@ class WC_Query {
* Returns a meta query to handle product visibility.
*
* @deprecated 3.0.0 Replaced with taxonomy.
* @param string $compare (default: 'IN')
* @param string $compare (default: 'IN').
* @return array
*/
public function visibility_meta_query( $compare = 'IN' ) {
@ -830,7 +898,7 @@ class WC_Query {
* Returns a meta query to handle product stock status.
*
* @deprecated 3.0.0 Replaced with taxonomy.
* @param string $status (default: 'instock')
* @param string $status (default: 'instock').
* @return array
*/
public function stock_status_meta_query( $status = 'instock' ) {
@ -869,6 +937,8 @@ class WC_Query {
/**
* Search post excerpt.
*
* @param string $where Where clause.
*
* @deprecated 3.2.0 - Not needed anymore since WordPress 4.5.
*/
public function search_post_excerpt( $where = '' ) {
@ -878,10 +948,10 @@ class WC_Query {
/**
* Remove the posts_where filter.
*
* @deprecated 3.2.0 - Nothing to remove anymore because search_post_excerpt() is deprecated.
*/
public function remove_posts_where() {
wc_deprecated_function( 'WC_Query::remove_posts_where', '3.2.0', 'Nothing to remove anymore because search_post_excerpt() is deprecated.' );
}
// @codingStandardsIgnoreEnd
}

View File

@ -190,7 +190,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
$query_args = array( 'attribute_name' => wc_variation_attribute_name( $attribute['name'] ) ) + $child_ids;
$values = array_unique(
$wpdb->get_col(
$wpdb->prepare( // wpcs: PreparedSQLPlaceholders replacement count ok.
$wpdb->prepare(
"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN {$query_in}", // @codingStandardsIgnoreLine.
$query_args
)
@ -661,6 +661,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
if ( $force_delete ) {
do_action( 'woocommerce_before_delete_product_variation', $variation_id );
wp_delete_post( $variation_id, true );
wp_delete_object_term_relationships( $variation_id, wc_get_attribute_taxonomy_names() );
do_action( 'woocommerce_delete_product_variation', $variation_id );
} else {
wp_trash_post( $variation_id );

View File

@ -473,10 +473,12 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
if ( $force || array_key_exists( 'attributes', $changes ) ) {
global $wpdb;
$product_id = $product->get_id();
$attributes = $product->get_attributes();
$updated_attribute_keys = array();
foreach ( $attributes as $key => $value ) {
update_post_meta( $product->get_id(), 'attribute_' . $key, wp_slash( $value ) );
update_post_meta( $product_id, 'attribute_' . $key, wp_slash( $value ) );
$updated_attribute_keys[] = 'attribute_' . $key;
}
@ -486,13 +488,27 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration
"SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d",
$wpdb->esc_like( 'attribute_' ) . '%',
$product->get_id()
$product_id
)
);
foreach ( $delete_attribute_keys as $key ) {
delete_post_meta( $product->get_id(), $key );
delete_post_meta( $product_id, $key );
}
// Set the attributes as regular taxonomy terms too...
$variation_attributes = array_keys( $product->get_variation_attributes( false ) );
foreach ( $attributes as $name => $value ) {
if ( '' !== $value && in_array( $name, $variation_attributes, true ) && term_exists( $value, $name ) ) {
wp_set_post_terms( $product_id, array( $value ), $name );
} elseif ( taxonomy_exists( $name ) ) {
wp_delete_object_term_relationships( $product_id, $name );
}
}
// ...and remove old taxonomy terms.
$attributes_to_delete = array_diff( wc_get_attribute_taxonomy_names(), array_keys( $attributes ) );
wp_delete_object_term_relationships( $product_id, $attributes_to_delete );
}
}

View File

@ -18,11 +18,13 @@ defined( 'ABSPATH' ) || exit;
function wc_template_redirect() {
global $wp_query, $wp;
// phpcs:disable WordPress.Security.NonceVerification.Recommended
// When default permalinks are enabled, redirect shop page to post type archive url.
if ( ! empty( $_GET['page_id'] ) && '' === get_option( 'permalink_structure' ) && wc_get_page_id( 'shop' ) === absint( $_GET['page_id'] ) && get_post_type_archive_link( 'product' ) ) { // WPCS: input var ok, CSRF ok.
if ( ! empty( $_GET['page_id'] ) && '' === get_option( 'permalink_structure' ) && wc_get_page_id( 'shop' ) === absint( $_GET['page_id'] ) && get_post_type_archive_link( 'product' ) ) {
wp_safe_redirect( get_post_type_archive_link( 'product' ) );
exit;
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
// When on the checkout with an empty cart, redirect to cart page.
if ( is_page( wc_get_page_id( 'checkout' ) ) && wc_get_page_id( 'checkout' ) !== wc_get_page_id( 'cart' ) && WC()->cart->is_empty() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_redirect_empty_cart', true ) ) {
@ -33,7 +35,7 @@ function wc_template_redirect() {
}
// Logout.
if ( isset( $wp->query_vars['customer-logout'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) { // WPCS: input var ok, CSRF ok.
if ( isset( $wp->query_vars['customer-logout'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) {
wp_safe_redirect( str_replace( '&amp;', '&', wp_logout_url( wc_get_page_permalink( 'myaccount' ) ) ) );
exit;
}
@ -96,9 +98,11 @@ add_action( 'template_redirect', 'wc_send_frame_options_header' );
* @since 2.5.3
*/
function wc_prevent_endpoint_indexing() {
if ( is_wc_endpoint_url() || isset( $_GET['download_file'] ) ) { // WPCS: input var ok, CSRF ok.
@header( 'X-Robots-Tag: noindex' ); // @codingStandardsIgnoreLine
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged
if ( is_wc_endpoint_url() || isset( $_GET['download_file'] ) ) {
@header( 'X-Robots-Tag: noindex' );
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged
}
add_action( 'template_redirect', 'wc_prevent_endpoint_indexing' );
@ -232,6 +236,29 @@ function wc_set_loop_prop( $prop, $value = '' ) {
$GLOBALS['woocommerce_loop'][ $prop ] = $value;
}
/**
* Set the current visbility for a product in the woocommerce_loop global.
*
* @since 4.4.0
* @param int $product_id Product it to cache visbiility for.
* @param bool $value The poduct visibility value to cache.
*/
function wc_set_loop_product_visibility( $product_id, $value ) {
wc_set_loop_prop( "product_visibility_$product_id", $value );
}
/**
* Gets the cached current visibility for a product from the woocommerce_loop global.
*
* @since 4.4.0
* @param int $product_id Product id to get the cached visibility for.
*
* @return bool|null The cached product visibility, or null if on visibility has been cached for that product.
*/
function wc_get_loop_product_visibility( $product_id ) {
return wc_get_loop_prop( "product_visibility_$product_id", null );
}
/**
* Should the WooCommerce loop be displayed?
*
@ -704,7 +731,8 @@ function wc_product_class( $class = '', $product_id = null ) {
*/
function wc_query_string_form_fields( $values = null, $exclude = array(), $current_key = '', $return = false ) {
if ( is_null( $values ) ) {
$values = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$values = $_GET;
} elseif ( is_string( $values ) ) {
$url_parts = wp_parse_url( $values );
$values = array();
@ -1022,7 +1050,8 @@ if ( ! function_exists( 'woocommerce_demo_store' ) ) {
$notice_id = md5( $notice );
echo apply_filters( 'woocommerce_demo_store', '<p class="woocommerce-store-notice demo_store" data-notice-id="' . esc_attr( $notice_id ) . '" style="display:none;">' . wp_kses_post( $notice ) . ' <a href="#" class="woocommerce-store-notice__dismiss-link">' . esc_html__( 'Dismiss', 'woocommerce' ) . '</a></p>', $notice ); // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo apply_filters( 'woocommerce_demo_store', '<p class="woocommerce-store-notice demo_store" data-notice-id="' . esc_attr( $notice_id ) . '" style="display:none;">' . wp_kses_post( $notice ) . ' <a href="#" class="woocommerce-store-notice__dismiss-link">' . esc_html__( 'Dismiss', 'woocommerce' ) . '</a></p>', $notice );
}
}
@ -1062,7 +1091,8 @@ if ( ! function_exists( 'woocommerce_page_title' ) ) {
$page_title = apply_filters( 'woocommerce_page_title', $page_title );
if ( $echo ) {
echo $page_title; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $page_title;
} else {
return $page_title;
}
@ -1087,7 +1117,8 @@ if ( ! function_exists( 'woocommerce_product_loop_start' ) ) {
$loop_start = apply_filters( 'woocommerce_product_loop_start', ob_get_clean() );
if ( $echo ) {
echo $loop_start; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $loop_start;
} else {
return $loop_start;
}
@ -1110,7 +1141,8 @@ if ( ! function_exists( 'woocommerce_product_loop_end' ) ) {
$loop_end = apply_filters( 'woocommerce_product_loop_end', ob_get_clean() );
if ( $echo ) {
echo $loop_end; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $loop_end;
} else {
return $loop_end;
}
@ -1139,7 +1171,8 @@ if ( ! function_exists( 'woocommerce_template_loop_category_title' ) ) {
echo esc_html( $category->name );
if ( $category->count > 0 ) {
echo apply_filters( 'woocommerce_subcategory_count_html', ' <mark class="count">(' . esc_html( $category->count ) . ')</mark>', $category ); // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo apply_filters( 'woocommerce_subcategory_count_html', ' <mark class="count">(' . esc_html( $category->count ) . ')</mark>', $category );
}
?>
</h2>
@ -1199,7 +1232,8 @@ if ( ! function_exists( 'woocommerce_taxonomy_archive_description' ) ) {
$term = get_queried_object();
if ( $term && ! empty( $term->description ) ) {
echo '<div class="term-description">' . wc_format_content( $term->description ) . '</div>'; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="term-description">' . wc_format_content( $term->description ) . '</div>';
}
}
}
@ -1220,7 +1254,8 @@ if ( ! function_exists( 'woocommerce_product_archive_description' ) ) {
if ( $shop_page ) {
$description = wc_format_content( $shop_page->post_content );
if ( $description ) {
echo '<div class="page-description">' . $description . '</div>'; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="page-description">' . $description . '</div>';
}
}
}
@ -1276,7 +1311,8 @@ if ( ! function_exists( 'woocommerce_template_loop_product_thumbnail' ) ) {
* Get the product thumbnail for the loop.
*/
function woocommerce_template_loop_product_thumbnail() {
echo woocommerce_get_product_thumbnail(); // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo woocommerce_get_product_thumbnail();
}
}
if ( ! function_exists( 'woocommerce_template_loop_price' ) ) {
@ -1368,7 +1404,9 @@ if ( ! function_exists( 'woocommerce_catalog_ordering' ) ) {
);
$default_orderby = wc_get_loop_prop( 'is_search' ) ? 'relevance' : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', '' ) );
$orderby = isset( $_GET['orderby'] ) ? wc_clean( wp_unslash( $_GET['orderby'] ) ) : $default_orderby; // WPCS: sanitization ok, input var ok, CSRF ok.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$orderby = isset( $_GET['orderby'] ) ? wc_clean( wp_unslash( $_GET['orderby'] ) ) : $default_orderby;
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( wc_get_loop_prop( 'is_search' ) ) {
$catalog_orderby_options = array_merge( array( 'relevance' => __( 'Relevance', 'woocommerce' ) ), $catalog_orderby_options );
@ -1700,7 +1738,8 @@ if ( ! function_exists( 'woocommerce_quantity_input' ) ) {
wc_get_template( 'global/quantity-input.php', $args );
if ( $echo ) {
echo ob_get_clean(); // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo ob_get_clean();
} else {
return ob_get_clean();
}
@ -1780,7 +1819,8 @@ if ( ! function_exists( 'woocommerce_sort_product_tabs' ) ) {
// Make sure the $tabs parameter is an array.
if ( ! is_array( $tabs ) ) {
trigger_error( 'Function woocommerce_sort_product_tabs() expects an array as the first parameter. Defaulting to empty array.' ); // @codingStandardsIgnoreLine
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
trigger_error( 'Function woocommerce_sort_product_tabs() expects an array as the first parameter. Defaulting to empty array.' );
$tabs = array();
}
@ -1817,7 +1857,8 @@ if ( ! function_exists( 'woocommerce_comments' ) ) {
* @param int $depth Depth.
*/
function woocommerce_comments( $comment, $args, $depth ) {
$GLOBALS['comment'] = $comment; // WPCS: override ok.
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$GLOBALS['comment'] = $comment;
wc_get_template(
'single-product/review.php',
array(
@ -2443,7 +2484,8 @@ if ( ! function_exists( 'woocommerce_output_product_categories' ) ) {
return false;
}
echo $args['before']; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['before'];
foreach ( $product_categories as $category ) {
wc_get_template(
@ -2454,7 +2496,8 @@ if ( ! function_exists( 'woocommerce_output_product_categories' ) ) {
);
}
echo $args['after']; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['after'];
return true;
}
@ -2839,7 +2882,8 @@ if ( ! function_exists( 'woocommerce_form_field' ) ) {
if ( $args['return'] ) {
return $field;
} else {
echo $field; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $field;
}
}
}
@ -2882,7 +2926,8 @@ if ( ! function_exists( 'get_product_search_form' ) ) {
return $form;
}
echo $form; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $form;
}
}
@ -2951,8 +2996,10 @@ if ( ! function_exists( 'wc_dropdown_variation_attribute_options' ) ) {
// Get selected value.
if ( false === $args['selected'] && $args['attribute'] && $args['product'] instanceof WC_Product ) {
$selected_key = 'attribute_' . sanitize_title( $args['attribute'] );
$args['selected'] = isset( $_REQUEST[ $selected_key ] ) ? wc_clean( wp_unslash( $_REQUEST[ $selected_key ] ) ) : $args['product']->get_variation_default_attribute( $args['attribute'] ); // WPCS: input var ok, CSRF ok, sanitization ok.
$selected_key = 'attribute_' . sanitize_title( $args['attribute'] );
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$args['selected'] = isset( $_REQUEST[ $selected_key ] ) ? wc_clean( wp_unslash( $_REQUEST[ $selected_key ] ) ) : $args['product']->get_variation_default_attribute( $args['attribute'] );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
$options = $args['options'];
@ -2999,7 +3046,8 @@ if ( ! function_exists( 'wc_dropdown_variation_attribute_options' ) ) {
$html .= '</select>';
echo apply_filters( 'woocommerce_dropdown_variation_attribute_options_html', $html, $args ); // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo apply_filters( 'woocommerce_dropdown_variation_attribute_options_html', $html, $args );
}
}
@ -3236,7 +3284,8 @@ if ( ! function_exists( 'wc_display_item_meta' ) ) {
$html = apply_filters( 'woocommerce_display_item_meta', $html, $item, $args );
if ( $args['echo'] ) {
echo $html; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $html;
} else {
return $html;
}
@ -3290,7 +3339,8 @@ if ( ! function_exists( 'wc_display_item_downloads' ) ) {
$html = apply_filters( 'woocommerce_display_item_downloads', $html, $item, $args );
if ( $args['echo'] ) {
echo $html; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $html;
} else {
return $html;
}
@ -3698,3 +3748,5 @@ function wc_get_pay_buttons() {
}
echo '</div>';
}
// phpcs:enable Generic.Commenting.Todo.TaskFound

View File

@ -28,6 +28,7 @@ function wc_update_200_file_paths() {
$old_file_path = trim( $existing_file_path->meta_value );
if ( ! empty( $old_file_path ) ) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
$file_paths = serialize( array( md5( $old_file_path ) => $old_file_path ) );
$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = '_file_paths', meta_value = %s WHERE meta_id = %d", $file_paths, $existing_file_path->meta_id ) );
@ -53,11 +54,11 @@ function wc_update_200_permalinks() {
$base_slug = $shop_page_id > 0 && get_post( $shop_page_id ) ? get_page_uri( $shop_page_id ) : 'shop';
$category_base = get_option( 'woocommerce_prepend_shop_page_to_urls' ) == 'yes' ? trailingslashit( $base_slug ) : '';
$category_base = 'yes' === get_option( 'woocommerce_prepend_shop_page_to_urls' ) ? trailingslashit( $base_slug ) : '';
$category_slug = get_option( 'woocommerce_product_category_slug' ) ? get_option( 'woocommerce_product_category_slug' ) : _x( 'product-category', 'slug', 'woocommerce' );
$tag_slug = get_option( 'woocommerce_product_tag_slug' ) ? get_option( 'woocommerce_product_tag_slug' ) : _x( 'product-tag', 'slug', 'woocommerce' );
if ( 'yes' == get_option( 'woocommerce_prepend_shop_page_to_products' ) ) {
if ( 'yes' === get_option( 'woocommerce_prepend_shop_page_to_products' ) ) {
$product_base = trailingslashit( $base_slug );
} else {
$product_slug = get_option( 'woocommerce_product_slug' );
@ -68,7 +69,7 @@ function wc_update_200_permalinks() {
}
}
if ( get_option( 'woocommerce_prepend_category_to_products' ) == 'yes' ) {
if ( 'yes' === get_option( 'woocommerce_prepend_category_to_products' ) ) {
$product_base .= trailingslashit( '%product_cat%' );
}
@ -90,16 +91,16 @@ function wc_update_200_permalinks() {
*/
function wc_update_200_subcat_display() {
// Update subcat display settings.
if ( get_option( 'woocommerce_shop_show_subcategories' ) == 'yes' ) {
if ( get_option( 'woocommerce_hide_products_when_showing_subcategories' ) == 'yes' ) {
if ( 'yes' === get_option( 'woocommerce_shop_show_subcategories' ) ) {
if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) {
update_option( 'woocommerce_shop_page_display', 'subcategories' );
} else {
update_option( 'woocommerce_shop_page_display', 'both' );
}
}
if ( get_option( 'woocommerce_show_subcategories' ) == 'yes' ) {
if ( get_option( 'woocommerce_hide_products_when_showing_subcategories' ) == 'yes' ) {
if ( 'yes' === get_option( 'woocommerce_show_subcategories' ) ) {
if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) {
update_option( 'woocommerce_category_archive_display', 'subcategories' );
} else {
update_option( 'woocommerce_category_archive_display', 'both' );
@ -128,7 +129,7 @@ function wc_update_200_taxrates() {
foreach ( $states as $state ) {
if ( '*' == $state ) {
if ( '*' === $state ) {
$state = '';
}
@ -160,7 +161,7 @@ function wc_update_200_taxrates() {
$location_type = ( 'postcode' === $tax_rate['location_type'] ) ? 'postcode' : 'city';
if ( '*' == $tax_rate['state'] ) {
if ( '*' === $tax_rate['state'] ) {
$tax_rate['state'] = '';
}
@ -246,7 +247,7 @@ function wc_update_200_line_items() {
)
);
// Add line item meta.
// Add line item meta.
if ( $item_id ) {
wc_add_order_item_meta( $item_id, '_qty', absint( $order_item['qty'] ) );
wc_add_order_item_meta( $item_id, '_tax_class', $order_item['tax_class'] );
@ -324,7 +325,7 @@ function wc_update_200_line_items() {
)
);
// Add line item meta.
// Add line item meta.
if ( $item_id ) {
wc_add_order_item_meta( $item_id, 'compound', absint( isset( $order_tax['compound'] ) ? $order_tax['compound'] : 0 ) );
wc_add_order_item_meta( $item_id, 'tax_amount', wc_clean( $order_tax['cart_tax'] ) );
@ -393,6 +394,8 @@ function wc_update_200_db_version() {
function wc_update_209_brazillian_state() {
global $wpdb;
// phpcs:disable WordPress.DB.SlowDBQuery
// Update brazillian state codes.
$wpdb->update(
$wpdb->postmeta,
@ -434,6 +437,8 @@ function wc_update_209_brazillian_state() {
'meta_value' => 'BH',
)
);
// phpcs:enable WordPress.DB.SlowDBQuery
}
/**
@ -492,6 +497,7 @@ function wc_update_210_file_paths() {
}
}
if ( $needs_update ) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
$new_value = serialize( $new_value );
$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = %s, meta_value = %s WHERE meta_id = %d", '_downloadable_files', $new_value, $existing_file_path->meta_id ) );
@ -857,6 +863,8 @@ function wc_update_240_api_keys() {
* @return void
*/
function wc_update_240_webhooks() {
// phpcs:disable WordPress.DB.SlowDBQuery
/**
* Webhooks.
* Make sure order.update webhooks get the woocommerce_order_edit_status hook.
@ -873,6 +881,8 @@ function wc_update_240_webhooks() {
$webhook = new WC_Webhook( $order_update_webhook->ID );
$webhook->set_topic( 'order.updated' );
}
// phpcs:enable WordPress.DB.SlowDBQuery
}
/**
@ -993,6 +1003,8 @@ function wc_update_250_currency() {
update_option( 'woocommerce_currency', 'LAK' );
}
// phpcs:disable WordPress.DB.SlowDBQuery
// Update LAK currency code.
$wpdb->update(
$wpdb->postmeta,
@ -1005,6 +1017,7 @@ function wc_update_250_currency() {
)
);
// phpcs:enable WordPress.DB.SlowDBQuery
}
/**
@ -1184,6 +1197,8 @@ function wc_update_260_db_version() {
* @return void
*/
function wc_update_300_webhooks() {
// phpcs:disable WordPress.DB.SlowDBQuery
/**
* Make sure product.update webhooks get the woocommerce_product_quick_edit_save
* and woocommerce_product_bulk_edit_save hooks.
@ -1200,6 +1215,8 @@ function wc_update_300_webhooks() {
$webhook = new WC_Webhook( $product_update_webhook->ID );
$webhook->set_topic( 'product.updated' );
}
// phpcs:enable WordPress.DB.SlowDBQuery
}
/**
@ -1601,7 +1618,7 @@ function wc_update_330_product_stock_status() {
AND t3.meta_key = '_backorders' AND ( t3.meta_value = 'yes' OR t3.meta_value = 'notify' )",
$min_stock_amount
)
); // WPCS: db call ok, unprepared SQL ok, cache ok.
);
if ( empty( $post_ids ) ) {
return;
@ -1609,12 +1626,14 @@ function wc_update_330_product_stock_status() {
$post_ids = array_map( 'absint', $post_ids );
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
// Set the status to onbackorder for those products.
$wpdb->query(
"UPDATE $wpdb->postmeta
SET meta_value = 'onbackorder'
WHERE meta_key = '_stock_status' AND post_id IN ( " . implode( ',', $post_ids ) . ' )'
); // WPCS: db call ok, unprepared SQL ok, cache ok.
);
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
}
/**
@ -2065,7 +2084,8 @@ function wc_update_390_move_maxmind_database() {
$new_path = apply_filters( 'woocommerce_geolocation_local_database_path', $new_path, 2 );
$new_path = apply_filters( 'woocommerce_maxmind_geolocation_database_path', $new_path );
@rename( $old_path, $new_path ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@rename( $old_path, $new_path );
}
/**
@ -2110,3 +2130,48 @@ function wc_update_400_reset_action_scheduler_migration_status() {
function wc_update_400_db_version() {
WC_Install::update_db_version( '4.0.0' );
}
/**
* Register attributes as terms for variable products, in increments of 100 products.
*
* @return bool true if there are more products to process.
*/
function wc_update_440_insert_attribute_terms_for_variable_products() {
$state_option_name = 'woocommerce_' . __FUNCTION__ . '_state';
$page = intval( get_option( $state_option_name, 1 ) );
$products = wc_get_products(
array(
'type' => 'variable',
'limit' => 100,
'page' => $page,
)
);
if ( empty( $products ) ) {
delete_option( $state_option_name );
return false;
}
$attribute_taxonomy_names = wc_get_attribute_taxonomy_names();
foreach ( $products as $product ) {
$variation_ids = $product->get_children();
foreach ( $variation_ids as $variation_id ) {
$variation = wc_get_product( $variation_id );
$variation_attributes = $variation->get_attributes();
foreach ( $variation_attributes as $attr_name => $attr_value ) {
wp_set_post_terms( $variation_id, array( $attr_value ), $attr_name );
}
$attributes_to_delete = array_diff( $attribute_taxonomy_names, array_keys( $variation_attributes ) );
wp_delete_object_term_relationships( $variation_id, $attributes_to_delete );
}
}
return update_option( $state_option_name, $page + 1 );
}
/**
* Update DB version.
*/
function wc_update_440_db_version() {
WC_Install::update_db_version( '4.4.0' );
}

View File

@ -344,49 +344,77 @@ class WC_Widget_Layered_Nav extends WC_Widget {
protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) {
global $wpdb;
$tax_query = WC_Query::get_main_tax_query();
$meta_query = WC_Query::get_main_meta_query();
$main_tax_query = $this->get_main_tax_query();
$meta_query = $this->get_main_meta_query();
if ( 'or' === $query_type ) {
foreach ( $tax_query as $key => $query ) {
if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) {
unset( $tax_query[ $key ] );
$non_variable_tax_query_sql = array( 'where' => '' );
$is_and_query = 'and' === $query_type;
foreach ( $main_tax_query as $key => $query ) {
if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) {
if ( $is_and_query ) {
$non_variable_tax_query_sql = $this->convert_tax_query_to_sql( array( $query ) );
}
unset( $main_tax_query[ $key ] );
}
}
$meta_query = new WP_Meta_Query( $meta_query );
$tax_query = new WP_Tax_Query( $tax_query );
$meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' );
$tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' );
$exclude_variable_products_tax_query_sql = $this->get_extra_tax_query_sql( 'product_type', array( 'variable' ), 'NOT IN' );
$meta_query_sql = ( new WP_Meta_Query( $meta_query ) )->get_sql( 'post', $wpdb->posts, 'ID' );
$main_tax_query_sql = $this->convert_tax_query_to_sql( $main_tax_query );
$term_ids_sql = '(' . implode( ',', array_map( 'absint', $term_ids ) ) . ')';
// Generate query.
$query = array();
$query['select'] = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) as term_count, terms.term_id as term_count_id";
$query['from'] = "FROM {$wpdb->posts}";
$query['join'] = "
INNER JOIN {$wpdb->term_relationships} AS term_relationships ON {$wpdb->posts}.ID = term_relationships.object_id
INNER JOIN {$wpdb->term_relationships} AS tr ON {$wpdb->posts}.ID = tr.object_id
INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id )
INNER JOIN {$wpdb->terms} AS terms USING( term_id )
" . $tax_query_sql['join'] . $meta_query_sql['join'];
{$main_tax_query_sql['join']} {$meta_query_sql['join']}"; // Not an omission, really no more JOINs required.
$variable_where_part = "
OR ({$wpdb->posts}.post_type = 'product_variation'
AND NOT EXISTS (
SELECT ID FROM {$wpdb->posts} AS parent
WHERE parent.ID = {$wpdb->posts}.post_parent AND parent.post_status NOT IN ('publish')
))
";
$search_sql = '';
$search = $this->get_main_search_query_sql();
if ( $search ) {
$search_sql = ' AND ' . $search;
}
$query['where'] = "
WHERE {$wpdb->posts}.post_type IN ( 'product' )
AND {$wpdb->posts}.post_status = 'publish'"
. $tax_query_sql['where'] . $meta_query_sql['where'] .
'AND terms.term_id IN (' . implode( ',', array_map( 'absint', $term_ids ) ) . ')';
WHERE
{$wpdb->posts}.post_status = 'publish'
{$main_tax_query_sql['where']} {$meta_query_sql['where']}
AND (
(
{$wpdb->posts}.post_type = 'product'
{$exclude_variable_products_tax_query_sql['where']}
{$non_variable_tax_query_sql['where']}
)
{$variable_where_part}
)
AND terms.term_id IN {$term_ids_sql}
{$search_sql}";
$search = WC_Query::get_main_search_query_sql();
$search = $this->get_main_search_query_sql();
if ( $search ) {
$query['where'] .= ' AND ' . $search;
}
$query['group_by'] = 'GROUP BY terms.term_id';
$query = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query );
$query = implode( ' ', $query );
$query_sql = implode( ' ', $query );
// We have a query - let's see if cached results of this query already exist.
$query_hash = md5( $query );
$query_hash = md5( $query_sql );
// Maybe store a transient of the count values.
$cache = apply_filters( 'woocommerce_layered_nav_count_maybe_cache', true );
@ -397,17 +425,88 @@ class WC_Widget_Layered_Nav extends WC_Widget {
}
if ( ! isset( $cached_counts[ $query_hash ] ) ) {
$results = $wpdb->get_results( $query, ARRAY_A ); // @codingStandardsIgnoreLine
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$results = $wpdb->get_results( $query_sql, ARRAY_A );
$counts = array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) );
$cached_counts[ $query_hash ] = $counts;
if ( true === $cache ) {
set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, DAY_IN_SECONDS );
}
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
}
return array_map( 'absint', (array) $cached_counts[ $query_hash ] );
}
/**
* Wrapper for WC_Query::get_main_tax_query() to ease unit testing.
*
* @since 4.4.0
* @return array
*/
protected function get_main_tax_query() {
return WC_Query::get_main_tax_query();
}
/**
* Wrapper for WC_Query::get_main_search_query_sql() to ease unit testing.
*
* @since 4.4.0
* @return string
*/
protected function get_main_search_query_sql() {
return WC_Query::get_main_search_query_sql();
}
/**
* Wrapper for WC_Query::get_main_search_queryget_main_meta_query to ease unit testing.
*
* @since 4.4.0
* @return array
*/
protected function get_main_meta_query() {
return WC_Query::get_main_meta_query();
}
/**
* Get a tax query SQL for a given set of taxonomy, terms and operator.
* Uses an intermediate WP_Tax_Query object.
*
* @since 4.4.0
* @param string $taxonomy Taxonomy name.
* @param array $terms Terms to include in the query.
* @param string $operator Query operator, as supported by WP_Tax_Query; e.g. "NOT IN".
*
* @return array
*/
private function get_extra_tax_query_sql( $taxonomy, $terms, $operator ) {
$query = array(
array(
'taxonomy' => $taxonomy,
'field' => 'slug',
'terms' => $terms,
'operator' => $operator,
'include_children' => false,
),
);
return $this->convert_tax_query_to_sql( $query );
}
/**
* Convert a tax query array to SQL using an intermediate WP_Tax_Query object.
*
* @since 4.4.0
* @param array $query Query array in the same format accepted by WP_Tax_Query constructor.
*
* @return array Query SQL as returned by WP_Tax_Query->get_sql.
*/
private function convert_tax_query_to_sql( $query ) {
global $wpdb;
return ( new WP_Tax_Query( $query ) )->get_sql( $wpdb->posts, 'ID' );
}
/**
* Show list based layered nav.
*
@ -442,8 +541,9 @@ class WC_Widget_Layered_Nav extends WC_Widget {
continue;
}
$filter_name = 'filter_' . wc_attribute_taxonomy_slug( $taxonomy );
$current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array(); // WPCS: input var ok, CSRF ok.
$filter_name = 'filter_' . wc_attribute_taxonomy_slug( $taxonomy );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array();
$current_filter = array_map( 'sanitize_title', $current_filter );
if ( ! in_array( $term->slug, $current_filter, true ) ) {
@ -487,7 +587,8 @@ class WC_Widget_Layered_Nav extends WC_Widget {
$term_html .= ' ' . apply_filters( 'woocommerce_layered_nav_count', '<span class="count">(' . absint( $count ) . ')</span>', $count, $term );
echo '<li class="woocommerce-widget-layered-nav-list__item wc-layered-nav-term ' . ( $option_is_set ? 'woocommerce-widget-layered-nav-list__item--chosen chosen' : '' ) . '">';
echo apply_filters( 'woocommerce_layered_nav_term_html', $term_html, $term, $link, $count ); // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.EscapeOutput.OutputNotEscaped
echo apply_filters( 'woocommerce_layered_nav_term_html', $term_html, $term, $link, $count );
echo '</li>';
}

View File

@ -20,7 +20,7 @@ defined( 'ABSPATH' ) || exit;
global $product;
// Ensure visibility.
if ( empty( $product ) || ! $product->is_visible() ) {
if ( empty( $product ) || false === wc_get_loop_product_visibility( $product->get_id() ) || ! $product->is_visible() ) {
return;
}
?>

View File

@ -23,7 +23,8 @@ if ( ! defined( 'ABSPATH' ) ) {
?>
<p class="woocommerce-result-count">
<?php
if ( 1 === $total ) {
// phpcs:disable WordPress.Security
if ( 1 === intval( $total ) ) {
_e( 'Showing the single result', 'woocommerce' );
} elseif ( $total <= $per_page || -1 === $per_page ) {
/* translators: %d: total results */
@ -34,5 +35,6 @@ if ( ! defined( 'ABSPATH' ) ) {
/* translators: 1: first result 2: last result 3: total results */
printf( _nx( 'Showing %1$d&ndash;%2$d of %3$d result', 'Showing %1$d&ndash;%2$d of %3$d results', $total, 'with first and last result', 'woocommerce' ), $first, $last, $total );
}
// phpcs:enable WordPress.Security
?>
</p>

View File

@ -103,14 +103,19 @@ class WC_Helper_Product {
}
/**
* Create a dummy variation product.
* Create a dummy variation product or configure an existing product object with dummy data.
*
*
* @since 2.3
*
* @param WC_Product_Variable|null $product Product object to configure, or null to create a new one.
* @return WC_Product_Variable
*/
public static function create_variation_product() {
$product = new WC_Product_Variable();
public static function create_variation_product( $product = null ) {
$is_new_product = is_null( $product );
if ( $is_new_product ) {
$product = new WC_Product_Variable();
}
$product->set_props(
array(
'name' => 'Dummy Variable Product',
@ -120,96 +125,132 @@ class WC_Helper_Product {
$attributes = array();
$attribute = new WC_Product_Attribute();
$attribute_data = self::create_attribute( 'size', array( 'small', 'large', 'huge' ) );
$attribute->set_id( $attribute_data['attribute_id'] );
$attribute->set_name( $attribute_data['attribute_taxonomy'] );
$attribute->set_options( $attribute_data['term_ids'] );
$attribute->set_position( 1 );
$attribute->set_visible( true );
$attribute->set_variation( true );
$attributes[] = $attribute;
$attribute = new WC_Product_Attribute();
$attribute_data = self::create_attribute( 'colour', array( 'red', 'blue' ) );
$attribute->set_id( $attribute_data['attribute_id'] );
$attribute->set_name( $attribute_data['attribute_taxonomy'] );
$attribute->set_options( $attribute_data['term_ids'] );
$attribute->set_position( 1 );
$attribute->set_visible( true );
$attribute->set_variation( true );
$attributes[] = $attribute;
$attribute = new WC_Product_Attribute();
$attribute_data = self::create_attribute( 'number', array( '0', '1', '2' ) );
$attribute->set_id( $attribute_data['attribute_id'] );
$attribute->set_name( $attribute_data['attribute_taxonomy'] );
$attribute->set_options( $attribute_data['term_ids'] );
$attribute->set_position( 1 );
$attribute->set_visible( true );
$attribute->set_variation( true );
$attributes[] = $attribute;
$attributes[] = self::create_product_attribute_object( 'size', array( 'small', 'large', 'huge' ) );
$attributes[] = self::create_product_attribute_object( 'colour', array( 'red', 'blue' ) );
$attributes[] = self::create_product_attribute_object( 'number', array( '0', '1', '2' ) );
$product->set_attributes( $attributes );
$product->save();
$variation_1 = new WC_Product_Variation();
$variation_1->set_props(
array(
'parent_id' => $product->get_id(),
'sku' => 'DUMMY SKU VARIABLE SMALL',
'regular_price' => 10,
)
);
$variation_1->set_attributes( array( 'pa_size' => 'small' ) );
$variation_1->save();
$variations = array();
$variation_2 = new WC_Product_Variation();
$variation_2->set_props(
array(
'parent_id' => $product->get_id(),
'sku' => 'DUMMY SKU VARIABLE LARGE',
'regular_price' => 15,
)
$variations[] = self::create_product_variation_object(
$product->get_id(),
'DUMMY SKU VARIABLE SMALL',
10,
array( 'pa_size' => 'small' )
);
$variation_2->set_attributes( array( 'pa_size' => 'large' ) );
$variation_2->save();
$variation_3 = new WC_Product_Variation();
$variation_3->set_props(
array(
'parent_id' => $product->get_id(),
'sku' => 'DUMMY SKU VARIABLE HUGE RED 0',
'regular_price' => 16,
)
$variations[] = self::create_product_variation_object(
$product->get_id(),
'DUMMY SKU VARIABLE LARGE',
15,
array( 'pa_size' => 'large' )
);
$variation_3->set_attributes(
$variations[] = self::create_product_variation_object(
$product->get_id(),
'DUMMY SKU VARIABLE HUGE RED 0',
16,
array(
'pa_size' => 'huge',
'pa_colour' => 'red',
'pa_number' => '0',
)
);
$variation_3->save();
$variation_4 = new WC_Product_Variation();
$variation_4->set_props(
array(
'parent_id' => $product->get_id(),
'sku' => 'DUMMY SKU VARIABLE HUGE RED 2',
'regular_price' => 17,
)
);
$variation_4->set_attributes(
$variations[] = self::create_product_variation_object(
$product->get_id(),
'DUMMY SKU VARIABLE HUGE RED 2',
17,
array(
'pa_size' => 'huge',
'pa_colour' => 'red',
'pa_number' => '2',
)
);
$variation_4->save();
return wc_get_product( $product->get_id() );
$variations[] = self::create_product_variation_object(
$product->get_id(),
'DUMMY SKU VARIABLE HUGE BLUE 2',
18,
array(
'pa_size' => 'huge',
'pa_colour' => 'blue',
'pa_number' => '2',
)
);
$variations[] = self::create_product_variation_object(
$product->get_id(),
'DUMMY SKU VARIABLE HUGE BLUE ANY NUMBER',
19,
array(
'pa_size' => 'huge',
'pa_colour' => 'blue',
'pa_number' => '',
)
);
if ( $is_new_product ) {
return wc_get_product( $product->get_id() );
}
$variation_ids = array_map(
function( $variation ) {
return $variation->get_id();
},
$variations
);
$product->set_children( $variation_ids );
return $product;
}
/**
* Creates an instance of WC_Product_Variation with the supplied parameters, optionally persisting it to the database.
*
* @param string $parent_id Parent product id.
* @param string $sku SKU for the variation.
* @param int $price Price of the variation.
* @param array $attributes Attributes that define the variation, e.g. ['pa_color'=>'red'].
* @param bool $save If true, the object will be saved to the database after being created and configured.
*
* @return WC_Product_Variation The created object.
*/
public static function create_product_variation_object( $parent_id, $sku, $price, $attributes, $save = true ) {
$variation = new WC_Product_Variation();
$variation->set_props(
array(
'parent_id' => $parent_id,
'sku' => $sku,
'regular_price' => $price,
)
);
$variation->set_attributes( $attributes );
if ( $save ) {
$variation->save();
}
return $variation;
}
/**
* Creates an instance of WC_Product_Attribute with the supplied parameters.
*
* @param string $raw_name Attribute raw name (without 'pa_' prefix).
* @param array $terms Possible values for the attribute.
*
* @return WC_Product_Attribute The created attribute object.
*/
public static function create_product_attribute_object( $raw_name = 'size', $terms = array( 'small' ) ) {
$attribute = new WC_Product_Attribute();
$attribute_data = self::create_attribute( $raw_name, $terms );
$attribute->set_id( $attribute_data['attribute_id'] );
$attribute->set_name( $attribute_data['attribute_taxonomy'] );
$attribute->set_options( $attribute_data['term_ids'] );
$attribute->set_position( 1 );
$attribute->set_visible( true );
$attribute->set_variation( true );
return $attribute;
}
/**

View File

@ -66,20 +66,20 @@ class WC_Tests_Admin_Duplicate_Product extends WC_Unit_Test_Case {
array(
'dummy-variable-product-small-2',
'dummy-variable-product-large-2',
'dummy-variable-product-3',
'dummy-variable-product-4',
),
array(
'dummy-variable-product-small-3',
'dummy-variable-product-large-3',
'dummy-variable-product-5',
'dummy-variable-product-6',
),
array(
'dummy-variable-product-small-3',
'dummy-variable-product-large-3',
'dummy-variable-product-9',
'dummy-variable-product-10',
),
array(
'dummy-variable-product-small-4',
'dummy-variable-product-large-4',
'dummy-variable-product-7',
'dummy-variable-product-8',
'dummy-variable-product-13',
'dummy-variable-product-14',
),
);
@ -88,7 +88,7 @@ class WC_Tests_Admin_Duplicate_Product extends WC_Unit_Test_Case {
$duplicate_children = $duplicate->get_children();
$this->assertEquals( 4, count( $duplicate_children ) );
$this->assertEquals( 6, count( $duplicate_children ) );
foreach ( $slug_match as $key => $slug ) {
$child = wc_get_product( $duplicate_children[ $key ] );
$this->assertEquals( $slug, $child->get_slug() );

View File

@ -2067,7 +2067,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
update_option( 'woocommerce_tax_round_at_subtotal', 'yes' );
WC()->cart->empty_cart();
$tax_rate = array(
$tax_rate = array(
'tax_rate_country' => '',
'tax_rate_state' => '',
'tax_rate' => '10.0000',
@ -2112,7 +2112,14 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$product = WC_Helper_Product::create_variation_product();
$variations = $product->get_available_variations();
$variation = array_pop( $variations );
$variation = current(
array_filter(
$variations,
function( $variation ) {
return 'DUMMY SKU VARIABLE HUGE RED 2' === $variation['sku'];
}
)
);
// Add variation with add_to_cart_action.
$_REQUEST['add-to-cart'] = $variation['variation_id'];
@ -2137,7 +2144,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$variation['variation_id'],
array(
'attribute_pa_size' => 'huge',
'attribute_pa_colour' => 'red',
'attribute_pa_colour' => 'red',
'attribute_pa_number' => '2',
)
);
@ -2166,7 +2173,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$variation = array_pop( $variations );
// Attempt adding variation with add_to_cart_action, specifying a different colour.
$_REQUEST['add-to-cart'] = $variation['variation_id'];
$_REQUEST['add-to-cart'] = $variation['variation_id'];
$_REQUEST['attribute_pa_colour'] = 'green';
WC_Form_Handler::add_to_cart_action( false );
$notices = WC()->session->get( 'wc_notices', array() );
@ -2197,7 +2204,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$variation = array_shift( $variations );
// Attempt adding variation with add_to_cart_action, specifying attributes not defined in the variation.
$_REQUEST['add-to-cart'] = $variation['variation_id'];
$_REQUEST['add-to-cart'] = $variation['variation_id'];
$_REQUEST['attribute_pa_colour'] = 'red';
$_REQUEST['attribute_pa_number'] = '1';
WC_Form_Handler::add_to_cart_action( false );
@ -2231,7 +2238,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$variation = array_shift( $variations );
// Attempt adding variation with add_to_cart_action, without specifying attribute_pa_colour.
$_REQUEST['add-to-cart'] = $variation['variation_id'];
$_REQUEST['add-to-cart'] = $variation['variation_id'];
$_REQUEST['attribute_pa_number'] = '0';
WC_Form_Handler::add_to_cart_action( false );
$notices = WC()->session->get( 'wc_notices', array() );

View File

@ -216,29 +216,38 @@ class WC_Tests_Product_Data_Store extends WC_Unit_Test_Case {
$product = new WC_Product_Variable( $product->get_id() );
$this->assertEquals( 4, count( $product->get_children() ) );
$this->assertEquals( 6, count( $product->get_children() ) );
$expected_prices['price'][ $children[0] ] = 8.00;
$expected_prices['price'][ $children[1] ] = 15.00;
$expected_prices['price'][ $children[2] ] = 16.00;
$expected_prices['price'][ $children[3] ] = 17.00;
$expected_prices['price'][ $children[4] ] = 18.00;
$expected_prices['price'][ $children[5] ] = 19.00;
$expected_prices['regular_price'][ $children[0] ] = 10.00;
$expected_prices['regular_price'][ $children[1] ] = 15.00;
$expected_prices['regular_price'][ $children[2] ] = 16.00;
$expected_prices['regular_price'][ $children[3] ] = 17.00;
$expected_prices['regular_price'][ $children[4] ] = 18.00;
$expected_prices['regular_price'][ $children[5] ] = 19.00;
$expected_prices['sale_price'][ $children[0] ] = 8.00;
$expected_prices['sale_price'][ $children[1] ] = 15.00;
$expected_prices['sale_price'][ $children[2] ] = 16.00;
$expected_prices['sale_price'][ $children[3] ] = 17.00;
$expected_prices['sale_price'][ $children[4] ] = 18.00;
$expected_prices['sale_price'][ $children[5] ] = 19.00;
$this->assertEquals( $expected_prices, $product->get_variation_prices() );
$expected_attributes = array(
'pa_size' => array( 'small', 'large', 'huge' ),
'pa_colour' => array( 'red' ),
'pa_number' => array( '0', '2' ),
'pa_colour' => array(
0 => 'red',
2 => 'blue',
),
'pa_number' => array( '0', '1', '2' ),
);
$this->assertEquals( $expected_attributes, $product->get_variation_attributes() );
}

View File

@ -5,9 +5,9 @@
* @since 2.3
*/
/**
* WC_Tests_Product_Functions class.
*/
/**
* WC_Tests_Product_Functions class.
*/
class WC_Tests_Product_Functions extends WC_Unit_Test_Case {
/**
@ -70,7 +70,7 @@ class WC_Tests_Product_Functions extends WC_Unit_Test_Case {
'type' => 'variation',
)
);
$this->assertCount( 4, $products );
$this->assertCount( 6, $products );
// Test parent.
$products = wc_get_products(
@ -80,7 +80,7 @@ class WC_Tests_Product_Functions extends WC_Unit_Test_Case {
'parent' => $variation->get_id(),
)
);
$this->assertCount( 4, $products );
$this->assertCount( 6, $products );
// Test parent_exclude.
$products = wc_get_products(

View File

@ -159,7 +159,7 @@ class WC_Tests_Product_Variable extends WC_Unit_Test_Case {
* @param string $expected_stock_status The expected stock status of the product after being saved.
*/
public function test_stock_status_on_save_when_managing_stock( $stock_quantity, $notify_no_stock_amount, $accepts_backorders, $expected_stock_status ) {
list($product, $child1, $child2) = $this->get_variable_product_with_children();
list( $product, $child1, $child2 ) = $this->get_variable_product_with_children();
update_option( 'woocommerce_notify_no_stock_amount', $notify_no_stock_amount );
@ -176,4 +176,199 @@ class WC_Tests_Product_Variable extends WC_Unit_Test_Case {
$this->assertEquals( $expected_stock_status, $product->get_stock_status() );
}
/**
* Setup for a test for is_visible.
*
* @param array $filtering_attributes Simulated filtering attributes as an array of attribute_name => [term1, term2...].
* @param bool $hide_out_of_stock_products Should the woocommerce_hide_out_of_stock_items option be set?.
* @param bool $is_visible_from_parent Return value of is_visible from base class.
*
* @return WC_Product_Variable A properly configured instance of WC_Product_Variable to test.
*/
private function prepare_visibility_test( $filtering_attributes, $hide_out_of_stock_products = true, $is_visible_from_parent = true ) {
foreach ( $filtering_attributes as $attribute_name => $terms ) {
$filtering_attributes[ $attribute_name ]['query_type'] = 'ANY_QUERY_TYPE';
$filtering_attributes[ $attribute_name ]['terms'] = $terms;
}
update_option( 'woocommerce_hide_out_of_stock_items', $hide_out_of_stock_products ? 'yes' : 'no' );
$sut = $this
->getMockBuilder( WC_Product_Variable::class )
->setMethods( array( 'parent_is_visible_core', 'get_layered_nav_chosen_attributes' ) )
->getMock();
$sut = WC_Helper_Product::create_variation_product( $sut, true );
$sut->save();
$sut->method( 'parent_is_visible_core' )->willReturn( $is_visible_from_parent );
$sut->method( 'get_layered_nav_chosen_attributes' )->willReturn( $filtering_attributes );
return $sut;
}
/**
* Configure the stock status for the attribute-based variations of a product.
*
* @param WC_Product_Variable $product Product with the variations to configure.
* @param array $attributes An array of attribute_name => [attribute_values], only the matching variations will have stock.
*/
private function set_variations_with_stock( $product, $attributes ) {
$variation_ids = $product->get_children();
foreach ( $variation_ids as $id ) {
$variation = wc_get_product( $id );
$attribute_matches = true;
foreach ( $attributes as $name => $values ) {
if ( ! in_array( $variation->get_attribute( $name ), $values, true ) ) {
$attribute_matches = false;
}
}
$variation->set_stock_status( $attribute_matches ? 'instock' : 'outofstock' );
$variation->save();
}
}
/**
* @testdox The product should be invisible when the parent 'is_visible' method returns false.
*/
public function test_is_invisible_when_parent_is_visible_returns_false() {
$sut = $this->prepare_visibility_test( array(), '', false, false );
$this->assertFalse( $sut->is_visible() );
}
/**
* @testdox The product should be visible when no nav filtering is supplied if at least one variation has stock.
*
* Note that if no variations have stock the base is_visible will already return false.
*/
public function test_is_visible_when_no_filtering_supplied_and_at_least_one_variation_has_stock() {
$sut = $this->prepare_visibility_test( array(), '' );
$this->set_variations_with_stock( $sut, array( 'pa_size' => array( 'small' ) ) );
$this->assertTrue( $sut->is_visible() );
}
/**
* @testdox Test product visibility when the variation requested in nav filtering has no stock, result depends on woocommerce_hide_out_of_stock_items option.
*
* @param bool $hide_out_of_stock Value for woocommerce_hide_out_of_stock_items.
* @param bool $expected_visibility Expected value of is_visible for the tested product.
*
* @testWith [true, false]
* [false, true]
*/
public function test_visibility_when_supplied_filter_has_no_stock( $hide_out_of_stock, $expected_visibility ) {
$sut = $this->prepare_visibility_test( array( 'pa_size' => array( 'large' ) ), $hide_out_of_stock );
$this->set_variations_with_stock( $sut, array( 'pa_size' => array( 'small' ) ) );
$this->assertEquals( $expected_visibility, $sut->is_visible() );
}
/**
* @testdox Product should always be visible when only one of the variations requested in nav filtering has stock.
*
* @param bool $hide_out_of_stock Value for woocommerce_hide_out_of_stock_items.
*
* @testWith [true]
* [false]
*/
public function test_visibility_when_multiple_filter_values_supplied_and_only_one_has_stock( $hide_out_of_stock ) {
$sut = $this->prepare_visibility_test( array( 'pa_size' => array( 'small', 'large' ) ), $hide_out_of_stock );
$this->set_variations_with_stock( $sut, array( 'pa_size' => array( 'small' ) ) );
$this->assertTrue( $sut->is_visible() );
}
/**
* @testdox Product should be visible when all of the variations requested in nav filtering have stock.
*
* @param bool $hide_out_of_stock Value for woocommerce_hide_out_of_stock_items.
*
* @testWith [true]
* [false]
*/
public function test_visibility_when_multiple_filter_values_supplied_and_all_of_them_have_stock( $hide_out_of_stock ) {
$sut = $this->prepare_visibility_test( array( 'pa_size' => array( 'small', 'large' ) ), $hide_out_of_stock );
$this->set_variations_with_stock( $sut, array( 'pa_size' => array( 'small', 'large' ) ) );
$this->assertTrue( $sut->is_visible() );
}
/**
* @testdox Product should be visible when multiple filters are present, and there's a variation matching all of them.
*/
public function test_visibility_when_multiple_filters_are_used_and_all_of_them_match() {
$sut = $this->prepare_visibility_test(
array(
'pa_size' => array( 'huge' ),
'pa_colour' => array( 'blue' ),
),
true
);
$this->set_variations_with_stock(
$sut,
array(
'pa_size' => array( 'huge' ),
'pa_colour' => array( 'blue' ),
'pa_number' => array( '2' ),
)
);
$this->assertTrue( $sut->is_visible() );
}
/**
* @testdox Product should not be visible when multiple filters are present, and there are no variations matching all of them.
*/
public function test_visibility_when_multiple_filters_are_used_and_one_of_them_does_not_match() {
$sut = $this->prepare_visibility_test(
array(
'pa_size' => array( 'small', 'huge' ),
'pa_colour' => array( 'red' ),
),
true
);
$this->set_variations_with_stock(
$sut,
array(
'pa_size' => array( 'huge' ),
'pa_colour' => array( 'blue' ),
'pa_number' => array( '2' ),
)
);
$this->assertFalse( $sut->is_visible() );
}
/**
* @testdox Attributes having "Any..." as value should not count when searching for matching attributes.
*/
public function test_visibility_when_multiple_filters_are_used_and_an_attribute_has_any_value() {
$sut = $this->prepare_visibility_test(
array(
'pa_size' => array( 'huge' ),
'pa_number' => array( '34' ),
),
true
);
$this->set_variations_with_stock(
$sut,
array(
'pa_size' => array( 'huge' ),
'pa_colour' => array( 'blue' ),
'pa_number' => array( '' ),
)
);
$this->assertTrue( $sut->is_visible() );
}
}

View File

@ -91,4 +91,43 @@ class WC_Tests_Product_Variation extends WC_Unit_Test_Case {
$variable_product = WC_Helper_Product::create_variation_product();
new WC_Product_Variation( $variable_product->get_id() );
}
/**
* @testdox Test that get_variation_attributes returns the appropriate values.
*
* @param bool $with_prefix Parameter for get_variation_attributes.
* @param string $expected_prefix Expected prefix on the returned attribute names.
*
* @testWith [true, "attribute_"]
* [false, ""]
*/
public function test_get_variation_attributes( $with_prefix, $expected_prefix ) {
$product = WC_Helper_Product::create_variation_product();
$sut = wc_get_product( $product->get_children()[2] );
$expected = array(
$expected_prefix . 'pa_size' => 'huge',
$expected_prefix . 'pa_colour' => 'red',
$expected_prefix . 'pa_number' => '0',
);
$actual = $sut->get_variation_attributes( $with_prefix );
$this->assertEquals( $expected, $actual );
}
/**
* @testdox Test that the delete method removes the attribute terms for the variation.
*/
public function test_delete_removes_attribute_terms() {
$product = WC_Helper_Product::create_variation_product();
$sut = wc_get_product( $product->get_children()[2] );
$id = $sut->get_id();
$sut->delete( true );
$attribute_names = wc_get_attribute_taxonomy_names();
$variation_attribute_terms = wp_get_post_terms( $id, $attribute_names );
$this->assertEmpty( $variation_attribute_terms );
}
}

View File

@ -28,6 +28,7 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
$this->assertTrue( wc_has_notice( 'test', 'error' ) );
// Clean up.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
unset( $_GET['wc_error'] );
wc_clear_notices();
@ -182,6 +183,7 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
* @group core-only
*/
public function test_get_catalog_ordering_args() {
// phpcs:disable WordPress.DB.SlowDBQuery
$data = array(
array(
'orderby' => 'menu_order',
@ -297,6 +299,7 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
),
),
);
// phpcs:enable WordPress.DB.SlowDBQuery
foreach ( $data as $test ) {
$result = WC()->query->get_catalog_ordering_args( $test['orderby'], $test['order'] );
@ -310,11 +313,13 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
public function test_get_catalog_ordering_args_GET() {
$_GET['orderby'] = 'price-desc';
// phpcs:disable WordPress.DB.SlowDBQuery
$expected = array(
'orderby' => 'price',
'order' => 'DESC',
'meta_key' => '',
);
// phpcs:enable WordPress.DB.SlowDBQuery
$this->assertEquals( $expected, WC()->query->get_catalog_ordering_args() );
@ -341,9 +346,11 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
'include_children' => true,
);
// phpcs:disable WordPress.DB.SlowDBQuery
$query_args = array(
'tax_query' => array( $tax_query ),
);
// phpcs:enable WordPress.DB.SlowDBQuery
WC()->query->product_query( new WP_Query( $query_args ) );
$tax_queries = WC_Query::get_main_tax_query();
@ -360,9 +367,11 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
'compare' => '=',
);
// phpcs:disable WordPress.DB.SlowDBQuery
$query_args = array(
'meta_query' => array( $meta_query ),
);
// phpcs:enable WordPress.DB.SlowDBQuery
WC()->query->product_query( new WP_Query( $query_args ) );
$meta_queries = WC_Query::get_main_meta_query();
@ -428,4 +437,84 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
WC()->query->remove_ordering_args();
}
/**
* Setup for a test for adjust_posts.
*
* @param bool $with_nav_filtering_data Should WC_Query::get_layered_nav_chosen_attributes return filtering data?.
* @param bool $use_objects If true, get_current_posts will return objects with an ID property; if false, it will returns the ids.
*
* @return array An array where the first element is the instance of WC_Query, and the second is an array of sample products created.
*/
private function setup_adjust_posts_test( $with_nav_filtering_data, $use_objects ) {
update_option( 'woocommerce_hide_out_of_stock_items', 'yes' );
if ( $with_nav_filtering_data ) {
$nav_filtering_data = array( 'pa_something' => array( 'terms' => array( 'foo', 'bar' ) ) );
} else {
$nav_filtering_data = array();
}
$products = array();
$posts = array();
for ( $i = 0; $i < 5; $i++ ) {
$product = WC_Helper_Product::create_simple_product();
array_push( $products, $product );
$post = $use_objects ? (object) array( 'ID' => $product->get_id() ) : $product->get_id();
array_push( $posts, $post );
}
$products[0]->set_stock_status( 'outofstock' );
$sut = $this
->getMockBuilder( WC_Query::class )
->setMethods( array( 'get_current_posts', 'get_layered_nav_chosen_attributes_inst' ) )
->getMock();
$sut->method( 'get_current_posts' )->willReturn( $posts );
$sut->method( 'get_layered_nav_chosen_attributes_inst' )->willReturn( $nav_filtering_data );
return array( $sut, $products );
}
/**
* @param bool $with_nav_filtering_data Should WC_Query::get_layered_nav_chosen_attributes return filtering data?.
* @param bool $use_objects If true, get_current_posts will return objects with an ID property; if false, it will returns the ids.
*
* @testdox adjust_posts should return the number of visible products and create product visibility loop variables
* @testWith [true, true]
* [false, false]
* [true, false]
* [false, true]
*/
public function test_adjust_posts_count_with_nav_filtering_attributes( $with_nav_filtering_data, $use_objects ) {
list($sut, $products) = $this->setup_adjust_posts_test( $with_nav_filtering_data, $use_objects );
$products[0]->set_stock_status( 'outofstock' );
$products[0]->save();
$products[1]->set_stock_status( 'outofstock' );
$products[1]->save();
$this->assertEquals( 3, $sut->adjust_posts_count( 34 ) );
$this->assertEquals( 3, wc_get_loop_prop( 'total' ) );
$this->assertEquals( false, wc_get_loop_product_visibility( $products[0]->get_id() ) );
$this->assertEquals( false, wc_get_loop_product_visibility( $products[1]->get_id() ) );
foreach ( array_slice( $products, 2 ) as $product ) {
$this->assertEquals( true, wc_get_loop_product_visibility( $product->get_id() ) );
}
}
/**
* @testdox adjust_posts should return the input unmodified if get_current_posts returns null.
*/
public function test_adjust_posts_count_when_there_are_no_posts() {
$sut = $this
->getMockBuilder( WC_Query::class )
->setMethods( array( 'get_current_posts', 'get_layered_nav_chosen_attributes_inst' ) )
->getMock();
$sut->method( 'get_current_posts' )->willReturn( null );
$this->assertEquals( 34, $sut->adjust_posts_count( 34 ) );
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* Data Store Tests for variable products: WC_Product_Variable_Data_Store.
*
* @package WooCommerce\Tests\Product
*/
/**
* Class WC_Tests_Product_Variable_Data_Store
*/
class WC_Tests_Product_Variable_Data_Store extends WC_Unit_Test_Case {
/**
* @testdox Test that "delete" on a variation removes the associated attribute terms too.
*/
public function test_attribute_terms_are_deleted_for_deleted_variations() {
$product = WC_Helper_Product::create_variation_product();
$variation = wc_get_product( $product->get_children()[2] );
$variation_id = $variation->get_id();
$sut = new WC_Product_Variable_Data_Store_CPT();
$sut->delete_variations( $product->get_id(), true );
$attribute_names = wc_get_attribute_taxonomy_names();
$variation_attribute_terms = wp_get_post_terms( $variation_id, $attribute_names );
$this->assertEmpty( $variation_attribute_terms );
}
}

View File

@ -0,0 +1,137 @@
<?php
/**
* Data Store Tests for product variations: WC_Product_Variation_Data_Store.
*
* @package WooCommerce\Tests\Product
*/
/**
* Class WC_Tests_Product_Variation_Data_Store
*/
class WC_Tests_Product_Variation_Data_Store extends WC_Unit_Test_Case {
/**
* Create and save a variable product with size and category attributes, then create a corresponding
* variation object with size "small" and color "red", and return it without saving it to database.
*
* @return WC_Product_Variation The created variation object.
*/
private function create_variation_object_for_existing_variable_product() {
$attr_size = WC_Helper_Product::create_product_attribute_object( 'size', array( 'small', 'large' ) );
$attr_color = WC_Helper_Product::create_product_attribute_object( 'color', array( 'red', 'blue' ) );
$product = new WC_Product_Variable();
$product->set_attributes( array( $attr_size, $attr_color ) );
$product->save();
$variation = WC_Helper_Product::create_product_variation_object(
$product->get_id(),
'SMALL RED THING',
10,
array(
'pa_size' => 'small',
'pa_color' => 'red',
)
);
return $variation;
}
/**
* Return a simplified list with the attribute terms for a variation object.
*
* @param int $variation_id Id of the variation product.
*
* @return array Attributes as an "attribute"=>"term" associative array.
*/
private function get_attribute_terms_for_variation( $variation_id ) {
$attribute_names = wc_get_attribute_taxonomy_names();
$variation_attribute_terms = wp_get_post_terms( $variation_id, $attribute_names );
$terms = array();
foreach ( $variation_attribute_terms as $term ) {
$terms[ $term->taxonomy ] = $term->name;
}
return $terms;
}
/**
* @testdox Test that attribute terms are created for new variations.
*/
public function test_attribute_terms_are_created_for_new_variations() {
$variation = $this->create_variation_object_for_existing_variable_product();
$sut = new WC_Product_Variation_Data_Store_CPT();
$sut->create( $variation );
$terms = $this->get_attribute_terms_for_variation( $variation->get_id() );
$expected = array(
'pa_size' => 'small',
'pa_color' => 'red',
);
$this->assertEquals( $expected, $terms );
$variation->set_attributes(
array(
'pa_size' => 'large',
'pa_color' => 'blue',
)
);
$sut->update( $variation );
$terms = $this->get_attribute_terms_for_variation( $variation->get_id() );
$expected = array(
'pa_size' => 'large',
'pa_color' => 'blue',
);
$this->assertEquals( $expected, $terms );
}
/**
* @testdox Test that attribute terms are updated for updated variations.
*/
public function test_attribute_terms_are_updated_for_modified_variations() {
$variation = $this->create_variation_object_for_existing_variable_product();
$sut = new WC_Product_Variation_Data_Store_CPT();
$sut->create( $variation );
$new_attributes = array(
'pa_size' => 'small',
'pa_color' => 'red',
);
$variation->set_attributes( $new_attributes );
$sut->update( $variation );
$terms = $this->get_attribute_terms_for_variation( $variation->get_id() );
$this->assertEquals( $new_attributes, $terms );
}
/**
* @testdox Test that attribute terms are removed for variations updated with "Any" value.
*/
public function test_attribute_terms_are_removed_for_variations_set_to_any_attribute_value() {
$variation = $this->create_variation_object_for_existing_variable_product();
$sut = new WC_Product_Variation_Data_Store_CPT();
$sut->create( $variation );
$new_attributes = array(
'pa_size' => 'small',
'pa_color' => '',
);
$variation->set_attributes( $new_attributes );
$sut->update( $variation );
$terms = $this->get_attribute_terms_for_variation( $variation->get_id() );
$expected = array( 'pa_size' => 'small' );
$this->assertEquals( $expected, $terms );
}
}

View File

@ -0,0 +1,479 @@
<?php
/**
* Testing WC_Widget_Layered_Nav functionality.
*
* @package WooCommerce/Tests/Widgets
*/
/**
* Class for testing WC_Widget_Layered_Nav functionality.
*/
class WC_Tests_Widget_Layered_Nav extends WC_Unit_Test_Case {
/**
* Get an instance of the tested widget, and simulate filtering in the incoming request.
*
* @param string $filter_operation Operation supplied in the filter, 'or' or 'and'.
* @param array $filter_colors Slugs of the colors supplied in the filters.
* @param array $filter_styles Slugs of the styles supplied in the filters.
*
* @return WC_Widget_Layered_Nav An instance of WC_Widget_Layered_Nav ready to test.
*/
private function get_widget( $filter_operation, $filter_colors = array(), $filter_styles = array() ) {
$tax_query = array(
'relation' => 'and',
0 => array(
'taxonomy' => 'product_visibility',
'terms' => array(
get_term_by( 'slug', 'outofstock', 'product_visibility' )->term_taxonomy_id,
get_term_by( 'slug', 'exclude-from-catalog', 'product_visibility' )->term_taxonomy_id,
),
'field' => 'term_taxonomy_id',
'operator' => 'NOT IN',
),
);
if ( ! empty( $filter_colors ) ) {
array_push(
$tax_query,
array(
'taxonomy' => 'pa_color',
'terms' => $filter_colors,
'field' => 'slug',
'operator' => $filter_operation,
)
);
}
if ( ! empty( $filter_styles ) ) {
array_push(
$tax_query,
array(
'taxonomy' => 'pa_style',
'terms' => $filter_styles,
'field' => 'slug',
'operator' => $filter_operation,
)
);
}
$sut = $this
->getMockBuilder( WC_Widget_Layered_Nav::class )
->setMethods( array( 'get_main_tax_query', 'get_main_meta_query', 'get_main_search_query_sql' ) )
->getMock();
$sut->method( 'get_main_tax_query' )->willReturn( $tax_query );
$sut->method( 'get_main_meta_query' )->willReturn( array() );
$sut->method( 'get_main_search_query_sql' )->willReturn( null );
return $sut;
}
/**
* Create a simple or variable product that has color attributes.
* If a variable product is created, a variation will be created for each color.
*
* @param string $name Name of the product.
* @param array $colors_in_stock Slugs of the colors whose variations will have stock. If null, a simple product is created.
* @param array $colors_disabled Slugs of the colors whose variations will be disabled, N/A for a simple product.
* @param array $styles Array where the key is the colors and the value is the style that will have the variation for that color.
*
* @return WC_Product_Simple|WC_Product_Variable The created product.
*/
private function create_colored_product( $name, $colors_in_stock, $colors_disabled = array(), $styles = array() ) {
$create_as_simple = is_null( $colors_in_stock );
$main_product = $create_as_simple ? new WC_Product_Simple() : new WC_Product_Variable();
$main_product->set_props(
array(
'name' => $name,
'sku' => 'SKU for' . $name,
)
);
$existing_colors = array( 'black', 'brown', 'blue', 'green', 'pink', 'yellow' );
$existing_styles = array( 'classic', 'sport' );
$attributes = array(
WC_Helper_Product::create_product_attribute_object( 'color', $existing_colors ),
WC_Helper_Product::create_product_attribute_object( 'style', $existing_styles ),
);
$main_product->set_attributes( $attributes );
$main_product->save();
if ( $create_as_simple ) {
return $main_product;
}
$variation_objects = array();
foreach ( $existing_colors as $color ) {
$variation_attributes = array(
'pa_color' => $color,
'pa_style' => array_key_exists( $color, $styles ) ? $styles[ $color ] : '',
);
$variation_object = WC_Helper_Product::create_product_variation_object(
$main_product->get_id(),
"SKU for $color $name",
10,
$variation_attributes
);
if ( ! in_array( $color, $colors_in_stock, true ) ) {
$variation_object->set_stock_status( 'outofstock' );
}
$variation_object->save();
if ( in_array( $color, $colors_disabled, true ) ) {
wp_update_post(
array(
'ID' => $variation_object->get_id(),
'post_status' => 'draft',
)
);
}
array_push( $variation_objects, $variation_object->get_id() );
}
$main_product->set_children( $variation_objects );
return $main_product;
}
/**
* Invoke a protected method in an object.
*
* @param object $object Object whose method will be invoked.
* @param string $method Name of the method to invoke.
* @param array $args Arguments for the method.
*
* @return mixed Result from the method invocation.
* @throws ReflectionException Error when dealing with reflection.
*/
private function invoke_protected( $object, $method, $args ) {
$class = new ReflectionClass( $object );
$method = $class->getMethod( $method );
$method->setAccessible( true );
return $method->invokeArgs( $object, $args );
}
/**
* Invokes the get_filtered_term_product_counts method on an instance the widget,
* for a given filtering request, and returns the resulting counts.
*
* @param string $operator Operator in the filtering request.
* @param array $colors Slugs of the colors included in the filtering request.
* @param array $styles Array where the key is the colors and the value is the style that will have the variation for that color.
*
* @return array An associative array where the keys are the color slugs and the values are the counts for each color.
* @throws ReflectionException Error when dealing with reflection to invoke the method.
*/
private function run_get_filtered_term_product_counts( $operator, $colors, $styles = array() ) {
$sut = $this->get_widget( $operator, $colors, $styles );
$color_terms = get_terms( 'pa_color', array( 'hide_empty' => '1' ) );
$color_term_ids = wp_list_pluck( $color_terms, 'term_id' );
$color_term_names = wp_list_pluck( $color_terms, 'slug' );
$color_names_by_id = array_combine( $color_term_ids, $color_term_names );
$counts = $this->invoke_protected(
$sut,
'get_filtered_term_product_counts',
array(
$color_term_ids,
'pa_color',
$operator,
)
);
$counts_by_name = array();
foreach ( $counts as $id => $count ) {
$counts_by_name[ $color_names_by_id[ $id ] ] = $count;
}
return $counts_by_name;
}
/**
* Changes the status of a post to 'draft'.
*
* @param int $post_id Id of the post to change.
*/
private function set_post_as_draft( $post_id ) {
global $wpdb;
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query( 'update ' . $wpdb->posts . " set post_status='draft' where ID=" . $post_id );
}
/**
* Data provider for test_product_count_per_attribute.
*
* @return array[]
*/
public function data_provider_for_test_product_count_per_attribute() {
return array(
// OR filtering, no attributes selected.
// Should count all the visible variations of all the products.
array(
'or',
array(),
false,
array(
'black' => 1,
'brown' => 2,
'blue' => 2,
'green' => 2,
'pink' => 1,
'yellow' => 1,
),
),
// OR filtering, some attributes selected
// (doesn't matter, the result is the same as in the previous case).
array(
'or',
array( 'black', 'green' ),
false,
array(
'black' => 1,
'brown' => 2,
'blue' => 2,
'green' => 2,
'pink' => 1,
'yellow' => 1,
),
),
// OR filtering, no attributes selected. Simple product is created too.
// Now it should include all the attributes of the simple product too.
array(
'or',
array(),
true,
array(
'black' => 2,
'brown' => 3,
'blue' => 3,
'green' => 3,
'pink' => 2,
'yellow' => 2,
),
),
// OR filtering, some attributes selected, Simple product is created too.
// Again, the attributes selected don't change the result.
array(
'or',
array( 'black', 'green' ),
true,
array(
'black' => 2,
'brown' => 3,
'blue' => 3,
'green' => 3,
'pink' => 2,
'yellow' => 2,
),
),
// AND filtering, no attributes selected.
// Should count all the visible variations of all the products as in the 'or' case.
array(
'and',
array(),
false,
array(
'black' => 1,
'brown' => 2,
'blue' => 2,
'green' => 2,
'pink' => 1,
'yellow' => 1,
),
),
// AND filtering, one attribute selected.
// Should still count the visible variations for all products as in the 'or' case.
array(
'and',
array( 'green' ),
false,
array(
'black' => 1,
'brown' => 2,
'blue' => 2,
'green' => 2,
'pink' => 1,
'yellow' => 1,
),
),
// AND filtering, more than one attribute selected.
// Should still count the visible variations for all products as in the 'or' case.
array(
'and',
array( 'green', 'pink' ),
false,
array(
'black' => 1,
'brown' => 2,
'blue' => 2,
'green' => 2,
'pink' => 1,
'yellow' => 1,
),
),
// AND filtering, no attributes selected, include simple product too.
// Same case as 'or': it should include all the attributes of the simple product too.
array(
'and',
array(),
true,
array(
'black' => 2,
'brown' => 3,
'blue' => 3,
'green' => 3,
'pink' => 2,
'yellow' => 2,
),
),
// AND filtering, select one attribute, include simple product too.
// Again, the simple product is now included in all counters, since it has the selected attribute.
array(
'and',
array( 'green' ),
true,
array(
'black' => 2,
'brown' => 3,
'blue' => 3,
'green' => 3,
'pink' => 2,
'yellow' => 2,
),
),
// AND filtering, select a couple of attributes, include simple product too.
// The simple product is still included too in all counters, since it has all of the selected attributes.
array(
'and',
array( 'green', 'pink' ),
true,
array(
'black' => 2,
'brown' => 3,
'blue' => 3,
'green' => 3,
'pink' => 2,
'yellow' => 2,
),
),
);
}
/**
* @testdox Test that the counters are correct for different filtering combinations, see the data provider method for details.
*
* @dataProvider data_provider_for_test_product_count_per_attribute
*
* @param string $filter_operator Filtering operator to use, 'or' or 'and'.
* @param array $filter_terms Slugs of the colors selected for filtering.
* @param bool $create_simple_product_too If true, create one simple product too. If false, create only the variable products.
* @param array $expected_counts An associative array where the keys are the color slugs and the values are the counts for each color.
*/
public function test_product_count_per_attribute( $filter_operator, $filter_terms, $create_simple_product_too, $expected_counts ) {
if ( $create_simple_product_too ) {
$this->create_colored_product( 'Something with many colors', null );
}
$this->create_colored_product( 'Big shoes', array( 'black', 'brown' ) );
$this->create_colored_product( 'Medium shoes', array( 'blue', 'brown' ) );
$this->create_colored_product( 'Small shoes', array( 'blue', 'green' ) );
$this->create_colored_product( 'Kids shoes', array( 'green', 'pink', 'yellow' ) );
$counts = $this->run_get_filtered_term_product_counts( $filter_operator, $filter_terms );
$this->assertEquals( $expected_counts, $counts );
}
/**
* @testdox Test that the counters are correct when using more than one filter simultaneously.
*
*/
public function test_product_count_per_multiple_attributes() {
$this->create_colored_product( 'Big shoes', array( 'black', 'brown' ) );
$this->create_colored_product(
'Medium shoes',
array( 'blue', 'brown' ),
array(),
array(
'blue' => 'sport',
'brown' => 'classic',
)
);
$this->create_colored_product(
'Small shoes',
array( 'blue', 'green' ),
array(),
array(
'blue' => 'classic',
'green' => 'sport',
)
);
$this->create_colored_product(
'Kids shoes',
array( 'green', 'pink', 'yellow', 'blue' ),
array(),
array(
'green' => 'classic',
'blue' => 'classic',
)
);
$counts = $this->run_get_filtered_term_product_counts( 'IN', array( 'blue' ), array( 'classic' ) );
$expected_counts = array(
'brown' => 1,
'blue' => 2,
'green' => 1,
);
$this->assertEquals( $expected_counts, $counts );
}
/**
* @testdox When a variable product is not published, none of its variations should be included in the counts.
*
* @throws ReflectionException Error when dealing with reflection to invoke the tested method.
*/
public function test_product_count_per_attribute_with_parent_not_published() {
$this->create_colored_product( 'Big shoes', array( 'black', 'brown' ) );
$medium = $this->create_colored_product( 'Medium shoes', array( 'blue', 'brown' ) );
$this->set_post_as_draft( $medium->get_id() );
$actual = $this->run_get_filtered_term_product_counts( 'or', array() );
$expected = array(
'black' => 1,
'brown' => 1,
);
$this->assertEquals( $expected, $actual );
}
/**
* @testdox When a variation is not published it should not be included in the counts (but other variations of the same product should).
*
* @throws ReflectionException Error when dealing with reflection to invoke the tested method.
*/
public function test_product_count_per_attribute_with_variation_not_published() {
$this->create_colored_product( 'Big shoes', array( 'black', 'brown' ) );
$this->create_colored_product( 'Medium shoes', array( 'blue', 'brown' ), array( 'brown' ) );
$actual = $this->run_get_filtered_term_product_counts( 'or', array() );
$expected = array(
'black' => 1,
'brown' => 1,
'blue' => 1,
);
$this->assertEquals( $expected, $actual );
}
}