ansient[ $query_args ] = $related_posts; } else { $transient = array( $query_args => $related_posts ); } set_transient( $transient_name, $transient, DAY_IN_SECONDS ); } $related_posts = apply_filters( 'woocommerce_related_products', $related_posts, $product_id, array( 'limit' => $limit, 'excluded_ids' => $exclude_ids, ) ); $related_posts = is_array( $related_posts ) ? $related_posts : array(); if ( apply_filters( 'woocommerce_product_related_posts_shuffle', true ) ) { shuffle( $related_posts ); } return array_slice( $related_posts, 0, $limit ); } /** * Retrieves product term ids for a taxonomy. * * @since 3.0.0 * @param int $product_id Product ID. * @param string $taxonomy Taxonomy slug. * @return array */ function wc_get_product_term_ids( $product_id, $taxonomy ) { $terms = get_the_terms( $product_id, $taxonomy ); return ( empty( $terms ) || is_wp_error( $terms ) ) ? array() : wp_list_pluck( $terms, 'term_id' ); } /** * For a given product, and optionally price/qty, work out the price with tax included, based on store settings. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @param array $args Optional arguments to pass product quantity and price. * @return float|string Price with tax included, or an empty string if price calculation failed. */ function wc_get_price_including_tax( $product, $args = array() ) { $args = wp_parse_args( $args, array( 'qty' => '', 'price' => '', ) ); $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : (float) $product->get_price(); $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; if ( empty( $qty ) ) { return 0.0; } $line_price = $price * $qty; $return_price = $line_price; if ( $product->is_taxable() ) { if ( ! wc_prices_include_tax() ) { // If the customer is exempt from VAT, set tax total to 0. if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) { $taxes_total = 0.00; } else { $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); $taxes = WC_Tax::calc_tax( $line_price, $tax_rates, false ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $taxes_total = array_sum( $taxes ); } else { $taxes_total = array_sum( array_map( 'wc_round_tax_total', $taxes ) ); } } $return_price = NumberUtil::round( $line_price + $taxes_total, wc_get_price_decimals() ); } else { $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); $base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); /** * If the customer is exempt from VAT, remove the taxes here. * Either remove the base or the user taxes depending on woocommerce_adjust_non_base_location_prices setting. */ if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) { // @codingStandardsIgnoreLine. $remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $remove_taxes_total = array_sum( $remove_taxes ); } else { $remove_taxes_total = array_sum( array_map( 'wc_round_tax_total', $remove_taxes ) ); } $return_price = NumberUtil::round( $line_price - $remove_taxes_total, wc_get_price_decimals() ); /** * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations. * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes. * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk. */ } elseif ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { $base_taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true ); $modded_taxes = WC_Tax::calc_tax( $line_price - array_sum( $base_taxes ), $tax_rates, false ); if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { $base_taxes_total = array_sum( $base_taxes ); $modded_taxes_total = array_sum( $modded_taxes ); } else { $base_taxes_total = array_sum( array_map( 'wc_round_tax_total', $base_taxes ) ); $modded_taxes_total = array_sum( array_map( 'wc_round_tax_total', $modded_taxes ) ); } $return_price = NumberUtil::round( $line_price - $base_taxes_total + $modded_taxes_total, wc_get_price_decimals() ); } } } return apply_filters( 'woocommerce_get_price_including_tax', $return_price, $qty, $product ); } /** * For a given product, and optionally price/qty, work out the price with tax excluded, based on store settings. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @param array $args Optional arguments to pass product quantity and price. * @return float|string Price with tax excluded, or an empty string if price calculation failed. */ function wc_get_price_excluding_tax( $product, $args = array() ) { if ( ! ( $product instanceof WC_Product ) ) { return ''; } $args = wp_parse_args( $args, array( 'qty' => '', 'price' => '', ) ); $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : (float) $product->get_price(); $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; if ( empty( $qty ) ) { return 0.0; } $line_price = $price * $qty; if ( $product->is_taxable() && wc_prices_include_tax() ) { $order = ArrayUtil::get_value_or_default( $args, 'order' ); $customer_id = $order ? $order->get_customer_id() : 0; $tax_rates = false; if ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { $tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); } elseif ( $customer_id ) { $customer = wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Customer::class, $customer_id ); $tax_rates = WC_Tax::get_rates( $product->get_tax_class(), $customer ); } elseif ( is_object( $order ) && method_exists( $order, 'get_taxable_location' ) ) { $tax_location = $order->get_taxable_location(); if ( is_array( $tax_location ) && isset( $tax_location['country'] ) ) { $tax_rates = WC_Tax::find_rates( array( 'country' => $tax_location['country'], 'state' => $tax_location['state'] ?? '', 'postcode' => $tax_location['postcode'] ?? '', 'city' => $tax_location['city'] ?? '', 'tax_class' => $product->get_tax_class(), ) ); } } // Fallback if no tax rates were determined. if ( false === $tax_rates ) { $tax_rates = WC_Tax::get_rates( $product->get_tax_class(), null ); } $remove_taxes = WC_Tax::calc_tax( $line_price, $tax_rates, true ); $return_price = $line_price - array_sum( $remove_taxes ); // Unrounded since we're dealing with tax inclusive prices. Matches logic in cart-totals class. @see adjust_non_base_location_price. } else { $return_price = $line_price; } return apply_filters( 'woocommerce_get_price_excluding_tax', $return_price, $qty, $product ); } /** * Returns the price including or excluding tax. * * By default it's based on the 'woocommerce_tax_display_shop' setting. * Set `$arg['display_context']` to 'cart' to base on the 'woocommerce_tax_display_cart' setting instead. * * @since 3.0.0 * @since 7.6.0 Added `display_context` argument. * * @param WC_Product $product WC_Product object. * @param array $args Optional arguments to pass product quantity and price. * @return float */ function wc_get_price_to_display( $product, $args = array() ) { $args = wp_parse_args( $args, array( 'qty' => 1, 'price' => $product->get_price(), 'display_context' => 'shop', ) ); $price = $args['price']; $qty = $args['qty']; $tax_display = get_option( 'cart' === $args['display_context'] ? 'woocommerce_tax_display_cart' : 'woocommerce_tax_display_shop' ); return TaxDisplayMode::INCLUSIVE === $tax_display ? wc_get_price_including_tax( $product, array( 'qty' => $qty, 'price' => $price, ) ) : wc_get_price_excluding_tax( $product, array( 'qty' => $qty, 'price' => $price, ) ); } /** * Returns the product categories in a list. * * @since 11.2.0 Added the `$orderby` argument. * * @param int $product_id Product ID. * @param string $sep (default: ', '). * @param string $before (default: ''). * @param string $after (default: ''). * @param string $orderby Optional ordering mode. Accepts 'name' to naturally order assigned terms by name, * ignoring the category order termmeta that the default path honors, or 'breadcrumb' * to order assigned terms by ancestry path, comparing category order, natural name, * and term ID at each level of the rendered hierarchy. Any other value preserves * WordPress term-list order; a non-empty unsupported value also triggers a * `_doing_it_wrong()` notice. Default empty string. * @return string|false|WP_Error */ function wc_get_product_category_list( $product_id, $sep = ', ', $before = '', $after = '', $orderby = '' ) { if ( ! in_array( $orderby, array( 'name', 'breadcrumb' ), true ) ) { if ( '' !== $orderby ) { wc_doing_it_wrong( __FUNCTION__, sprintf( /* translators: 1: the unsupported ordering mode that was passed. */ __( 'Unsupported $orderby value "%1$s". Use "name", "breadcrumb", or an empty string for WordPress term-list order.', 'woocommerce' ), is_scalar( $orderby ) ? (string) $orderby : gettype( $orderby ) ), '11.2.0' ); } return get_the_term_list( $product_id, 'product_cat', $before, $sep, $after ); } $terms = get_the_terms( $product_id, 'product_cat' ); if ( is_wp_error( $terms ) ) { return $terms; } if ( empty( $terms ) || ! is_array( $terms ) ) { return false; } $terms = array_values( $terms ); $should_sort = 1 < count( $terms ); if ( 'name' === $orderby && $should_sort ) { usort( $terms, function ( $a, $b ) { $name_comparison = strnatcasecmp( (string) $a->name, (string) $b->name ); return 0 !== $name_comparison ? $name_comparison : $a->term_id <=> $b->term_id; } ); } elseif ( 'breadcrumb' === $orderby && $should_sort ) { $rendered_terms = array(); foreach ( $terms as $term ) { $rendered_terms[ (int) $term->term_id ] = $term; } /* * Resolve ancestors one batched query per hierarchy level rather than one query per * ancestor. The batch is a cache prime, not the ordering data: get_ancestors() below reads * it, and so does get_term_link() further down, because product_cat is registered with a * hierarchical rewrite and therefore resolves each category's ancestry to build its * permalink. Priming here makes the deep leaf-only case cheaper than not ordering at all. */ $known_ids = array_fill_keys( array_keys( $rendered_terms ), true ); $frontier = array(); foreach ( $terms as $term ) { if ( $term->parent ) { $frontier[ (int) $term->parent ] = true; } } // Ancestry is filterable, so bound the walk rather than trusting it to terminate. $remaining_levels = 100; while ( $frontier && $remaining_levels-- > 0 ) { $frontier_ids = array_values( array_diff( array_keys( $frontier ), array_keys( $known_ids ) ) ); if ( ! $frontier_ids ) { break; } $frontier_parents = get_terms( array( 'taxonomy' => 'product_cat', 'include' => $frontier_ids, 'hide_empty' => false, 'fields' => 'id=>parent', 'orderby' => 'include', 'update_term_meta_cache' => false, ) ); /* * A filtered get_terms() can return any shape. Failing to prime is harmless; the * get_ancestors() calls below just query for themselves. */ if ( is_wp_error( $frontier_parents ) || ! is_array( $frontier_parents ) ) { break; } $frontier = array(); foreach ( $frontier_parents as $term_id => $parent_id ) { if ( ! is_numeric( $term_id ) || ! is_numeric( $parent_id ) ) { continue; } $known_ids[ (int) $term_id ] = true; if ( (int) $parent_id ) { $frontier[ (int) $parent_id ] = true; } } } update_termmeta_cache( array_keys( $rendered_terms ) ); $term_orders = array(); $term_paths = array(); foreach ( $terms as $term ) { $term_id = (int) $term->term_id; $term_order = get_term_meta( $term_id, 'order', true ); $term_orders[ $term_id ] = is_numeric( $term_order ) ? (int) $term_order : 0; $ancestor_ids = array_filter( (array) get_ancestors( $term_id, 'product_cat', 'taxonomy' ), 'is_numeric' ); $ancestor_ids = array_map( 'intval', $ancestor_ids ); // Cyclic ancestry can list the term itself among its own ancestors. $ancestor_ids = array_diff( array_unique( $ancestor_ids ), array( $term_id ) ); $path = array_reverse( array_values( $ancestor_ids ) ); $path[] = $term_id; /* * Rank on rendered terms only. Ancestors establish which term sits under which, but a * category that is not in the output never contributes a sort key of its own, so the * reason for the rendered order is always visible on the page. */ $term_paths[ $term_id ] = array_values( array_filter( $path, static function ( $step ) use ( $rendered_terms ) { return isset( $rendered_terms[ $step ] ); } ) ); } usort( $terms, static function ( $a, $b ) use ( $rendered_terms, $term_orders, $term_paths ) { $a_path = $term_paths[ (int) $a->term_id ]; $b_path = $term_paths[ (int) $b->term_id ]; $shared_depth = min( count( $a_path ), count( $b_path ) ); for ( $index = 0; $index < $shared_depth; $index++ ) { $a_term_id = $a_path[ $index ]; $b_term_id = $b_path[ $index ]; if ( $a_term_id === $b_term_id ) { continue; } if ( $term_orders[ $a_term_id ] !== $term_orders[ $b_term_id ] ) { return $term_orders[ $a_term_id ] <=> $term_orders[ $b_term_id ]; } $path_comparison = strnatcasecmp( (string) $rendered_terms[ $a_term_id ]->name, (string) $rendered_terms[ $b_term_id ]->name ); return 0 !== $path_comparison ? $path_comparison : $a_term_id <=> $b_term_id; } return count( $a_path ) <=> count( $b_path ); } ); } $links = array(); foreach ( $terms as $term ) { $link = get_term_link( $term, 'product_cat' ); if ( is_wp_error( $link ) ) { return $link; } $links[] = ''; } /* * Fire core's own term_links-{$taxonomy} hook, so that ordered output stays filterable by exactly * what already filters get_the_term_list(). */ /** This filter is documented in wp-includes/category-template.php */ $term_links = apply_filters( 'term_links-product_cat', $links ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores, WooCommerce.Commenting.CommentHooks.MissingSinceComment /* * Validate what the filter handed back. Core's own path does not, and passing a non-array to * implode() there raises a TypeError. Returning false keeps this path within the documented * string|false|WP_Error contract instead; core's behaviour is deliberately left alone, since * changing it would be a separate compatibility decision. */ if ( is_wp_error( $term_links ) ) { return $term_links; } if ( ! is_array( $term_links ) ) { return false; } return $before . implode( $sep, $term_links ) . $after; } /** * Returns the product tags in a list. * * @param int $product_id Product ID. * @param string $sep (default: ', '). * @param string $before (default: ''). * @param string $after (default: ''). * @return string */ function wc_get_product_tag_list( $product_id, $sep = ', ', $before = '', $after = '' ) { return get_the_term_list( $product_id, 'product_tag', $before, $sep, $after ); } /** * Callback for array filter to get visible only. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_visible( $product ) { return $product && is_a( $product, 'WC_Product' ) && $product->is_visible(); } /** * Callback for array filter to get visible grouped products only. * * @since 3.1.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_visible_grouped( $product ) { return $product && is_a( $product, 'WC_Product' ) && $product->is_viewable(); } /** * Callback for array filter to get products the user can edit only. * * @since 3.0.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_editable( $product ) { return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'edit_product', $product->get_id() ); } /** * Callback for array filter to get products the user can view only. * * @since 3.4.0 * @param WC_Product $product WC_Product object. * @return bool */ function wc_products_array_filter_readable( $product ) { return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'read_product', $product->get_id() ); } /** * Sort an array of products by a value. * * @since 3.0.0 * * @param array $products List of products to be ordered. * @param string $orderby Optional order criteria. * @param string $order Ascending or descending order. * * @return array */ function wc_products_array_orderby( $products, $orderby = 'date', $order = 'desc' ) { $orderby = strtolower( $orderby ); $order = strtolower( $order ); switch ( $orderby ) { case 'title': case 'id': case 'date': case 'modified': case 'menu_order': case 'price': usort( $products, 'wc_products_array_orderby_' . $orderby ); break; case 'none': break; default: shuffle( $products ); break; } if ( 'desc' === $order ) { $products = array_reverse( $products ); } return $products; } /** * Sort by title. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_title( $a, $b ) { return strcasecmp( $a->get_name(), $b->get_name() ); } /** * Sort by id. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_id( $a, $b ) { if ( $a->get_id() === $b->get_id() ) { return 0; } return ( $a->get_id() < $b->get_id() ) ? -1 : 1; } /** * Sort by date. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_date( $a, $b ) { if ( $a->get_date_created() === $b->get_date_created() ) { return 0; } return ( $a->get_date_created() < $b->get_date_created() ) ? -1 : 1; } /** * Sort by modified. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_modified( $a, $b ) { if ( $a->get_date_modified() === $b->get_date_modified() ) { return 0; } return ( $a->get_date_modified() < $b->get_date_modified() ) ? -1 : 1; } /** * Sort by menu order. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_menu_order( $a, $b ) { if ( $a->get_menu_order() === $b->get_menu_order() ) { return 0; } return ( $a->get_menu_order() < $b->get_menu_order() ) ? -1 : 1; } /** * Sort by price low to high. * * @since 3.0.0 * @param WC_Product $a First WC_Product object. * @param WC_Product $b Second WC_Product object. * @return int */ function wc_products_array_orderby_price( $a, $b ) { if ( $a->get_price() === $b->get_price() ) { return 0; } return ( $a->get_price() < $b->get_price() ) ? -1 : 1; } /** * Queue a product for syncing at the end of the request. * * @param int $product_id Product ID. */ function wc_deferred_product_sync( $product_id ) { global $wc_deferred_product_sync; if ( empty( $wc_deferred_product_sync ) ) { $wc_deferred_product_sync = array(); } $wc_deferred_product_sync[] = $product_id; } /** * See if the lookup table is being generated already. * * @since 3.6.0 * @return bool */ function wc_update_product_lookup_tables_is_running() { $table_updates_pending = WC()->queue()->search( array( 'status' => 'pending', 'group' => 'wc_update_product_lookup_tables', 'per_page' => 1, ) ); return (bool) count( $table_updates_pending ); } /** * Populate lookup table data for products. * * @since 3.6.0 */ function wc_update_product_lookup_tables() { global $wpdb; $is_cli = Constants::is_true( 'WP_CLI' ); $reviews_enabled = wc_reviews_enabled(); // Note that the table is not yet generated. update_option( 'woocommerce_product_lookup_table_is_generating', true ); // Make a row per product in lookup table. $wpdb->query( " INSERT IGNORE INTO {$wpdb->wc_product_meta_lookup} (`product_id`) SELECT posts.ID FROM {$wpdb->posts} posts WHERE posts.post_type IN ('product', 'product_variation') " ); // List of column names in the lookup table we need to populate. $columns = array( 'min_max_price', 'stock_quantity', 'sku', 'global_unique_id', 'stock_status', 'average_rating', 'total_sales', 'downloadable', 'virtual', 'onsale', 'tax_class', 'tax_status', // When last column is updated, woocommerce_product_lookup_table_is_generating is updated. ); foreach ( $columns as $index => $column ) { if ( 'average_rating' === $column && ! $reviews_enabled ) { continue; } if ( $is_cli ) { wc_update_product_lookup_tables_column( $column ); } else { WC()->queue()->schedule_single( time() + $index, 'wc_update_product_lookup_tables_column', array( 'column' => $column, ), 'wc_update_product_lookup_tables' ); } } if ( ! $reviews_enabled ) { return; } // Rating counts are serialised so they have to be unserialised before populating the lookup table. if ( $is_cli ) { $rating_count_rows = $wpdb->get_results( " SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_wc_rating_count' AND meta_value != '' AND meta_value != 'a:0:{}' ", ARRAY_A ); wc_update_product_lookup_tables_rating_count( $rating_count_rows ); } else { WC()->queue()->schedule_single( time() + 10, 'wc_update_product_lookup_tables_rating_count_batch', array( 'offset' => 0, 'limit' => 50, ), 'wc_update_product_lookup_tables' ); } } /** * Populate lookup table column data. * * @since 3.6.0 * @param string $column Column name to set. */ function wc_update_product_lookup_tables_column( $column ) { if ( empty( $column ) ) { return; } if ( 'average_rating' === $column && ! wc_reviews_enabled() ) { return; } global $wpdb; switch ( $column ) { case 'min_max_price': $wpdb->query( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table INNER JOIN ( SELECT lookup_table.product_id, MIN( meta_value+0 ) as min_price, MAX( meta_value+0 ) as max_price FROM {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' WHERE meta1.meta_value <> '' GROUP BY lookup_table.product_id ) as source on source.product_id = lookup_table.product_id SET lookup_table.min_price = source.min_price, lookup_table.max_price = source.max_price " ); break; case 'stock_quantity': $wpdb->query( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_manage_stock' LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_stock' SET lookup_table.stock_quantity = meta2.meta_value WHERE meta1.meta_value = 'yes' " ); break; case 'sku': case 'global_unique_id': case 'stock_status': case 'average_rating': case 'total_sales': case 'tax_class': case 'tax_status': if ( 'total_sales' === $column ) { $meta_key = 'total_sales'; } elseif ( 'average_rating' === $column ) { $meta_key = '_wc_average_rating'; } else { $meta_key = '_' . $column; } $column = esc_sql( $column ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta ON lookup_table.product_id = meta.post_id AND meta.meta_key = %s SET lookup_table.`{$column}` = meta.meta_value ", $meta_key ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared break; case 'downloadable': case 'virtual': $column = esc_sql( $column ); $meta_key = '_' . $column; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = %s SET lookup_table.`{$column}` = IF ( meta1.meta_value = 'yes', 1, 0 ) ", $meta_key ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared break; case 'onsale': $column = esc_sql( $column ); $decimals = absint( wc_get_price_decimals() ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( " UPDATE {$wpdb->wc_product_meta_lookup} lookup_table LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_sale_price' LEFT JOIN {$wpdb->postmeta} meta3 ON lookup_table.product_id = meta3.post_id AND meta3.meta_key = '_regular_price' SET lookup_table.`{$column}` = IF ( CAST( meta1.meta_value AS DECIMAL ) >= 0 AND CAST( meta2.meta_value AS CHAR ) != '' AND CAST( meta1.meta_value AS DECIMAL( 10, %d ) ) = CAST( meta2.meta_value AS DECIMAL( 10, %d ) ) AND CAST( meta3.meta_value AS DECIMAL( 10, %d ) ) > CAST( meta2.meta_value AS DECIMAL( 10, %d ) ) , 1, 0 ) ", $decimals, $decimals, $decimals, $decimals ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared break; } // Final column - mark complete. if ( 'tax_status' === $column ) { delete_option( 'woocommerce_product_lookup_table_is_generating' ); } } add_action( 'wc_update_product_lookup_tables_column', 'wc_update_product_lookup_tables_column' ); /** * Populate rating count lookup table data for products. * * @since 3.6.0 * @param array $rows Rows of rating counts to update in lookup table. */ function wc_update_product_lookup_tables_rating_count( $rows ) { if ( ! $rows || ! is_array( $rows ) ) { return; } global $wpdb; foreach ( $rows as $row ) { $count = array_sum( (array) maybe_unserialize( $row['meta_value'] ) ); $wpdb->update( $wpdb->wc_product_meta_lookup, array( 'rating_count' => absint( $count ), ), array( 'product_id' => absint( $row['post_id'] ), ) ); } } /** * Populate a batch of rating count lookup table data for products. * * @since 3.6.2 * @param array $offset Offset to query. * @param array $limit Limit to query. */ function wc_update_product_lookup_tables_rating_count_batch( $offset = 0, $limit = 0 ) { global $wpdb; if ( ! $limit || ! wc_reviews_enabled() ) { return; } $rating_count_rows = $wpdb->get_results( $wpdb->prepare( " SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_wc_rating_count' AND meta_value != '' AND meta_value != 'a:0:{}' ORDER BY post_id ASC LIMIT %d, %d ", $offset, $limit ), ARRAY_A ); if ( $rating_count_rows ) { wc_update_product_lookup_tables_rating_count( $rating_count_rows ); WC()->queue()->schedule_single( time() + 1, 'wc_update_product_lookup_tables_rating_count_batch', array( 'offset' => $offset + $limit, 'limit' => $limit, ), 'wc_update_product_lookup_tables' ); } } add_action( 'wc_update_product_lookup_tables_rating_count_batch', 'wc_update_product_lookup_tables_rating_count_batch', 10, 2 ); /** * Attach product featured image. Use image filename to match a product sku when product is not provided. * * @since 8.5.0 * @param int $attachment_id Media attachment ID. * @param WC_Product $product Optional product object. * @param bool $save_product If true, the changes in the product will be saved before the method returns. * @return void */ function wc_product_attach_featured_image( $attachment_id, $product = null, $save_product = true ) { $attachment_post = get_post( $attachment_id ); if ( ! $attachment_post ) { return; } if ( null === $product && wc_get_container()->get( MatchImageBySKU::class )->is_enabled() ) { // On upload the attachment post title is the uploaded file's filename. $file_name = pathinfo( $attachment_post->post_title, PATHINFO_FILENAME ); if ( ! $file_name ) { return; } $product_id = wc_get_product_id_by_sku( $file_name ); $product = wc_get_product( $product_id ); } if ( ! $product || ! current_user_can( 'edit_product', $product->get_id() ) ) { return; } $product->set_image_id( $attachment_id ); if ( $save_product ) { $product->save(); } if ( 0 === $attachment_post->post_parent ) { wp_update_post( array( 'ID' => $attachment_id, 'post_parent' => $product->get_id(), ) ); } } add_action( 'add_attachment', 'wc_product_attach_featured_image' );