WooCommerce Code Reference

wc-product-functions.php

Functions

wc_get_products()

Standard way of retrieving products based on certain parameters.

wc_get_products(array<string|int, mixed> $args) : array<string|int, mixed>|stdClass

This function should be used for product retrieval so that we have a data agnostic way to get a list of products.

Args and usage: https://developer.woocommerce.com/docs/extensions/core-concepts/wc-get-products/

Parameters
$args : array<string|int, mixed>

Array of args (above).

Tags
since
3.0.0

wc_get_product()

Main function for returning products, uses the WC_Product_Factory class.

wc_get_product([mixed $the_product = false ][, array<string|int, mixed> $deprecated = array() ]) : WC_Product|null|false

This function should only be called after 'init' action is finished, as there might be taxonomies that are getting registered during the init action.

Parameters
$the_product : mixed = false

Post object or post ID of the product.

$deprecated : array<string|int, mixed> = array()

Previously used to pass arguments to the factory, e.g. to force a type.

Tags
since
2.2.0

wc_get_product_object()

Get a product object.

wc_get_product_object(string $product_type, int $product_id) : WC_Product
Parameters
$product_type : string

Product type. If used an invalid type a WC_Product_Simple instance will be returned.

$product_id : int

Product ID.

Tags
see
WC_Product_Factory::get_product_classname
since
3.9.0

wc_product_dimensions_enabled()

Returns whether or not product dimensions (HxWxD) are enabled.

wc_product_dimensions_enabled() : bool

wc_delete_product_transients()

Clear transient cache for product data.

wc_delete_product_transients(int $post_id) : mixed
Parameters
$post_id : int

(default: 0) The product ID.

Delete all related products transients when a product is updated/created.

wc_delete_related_product_transients(int $post_id) : mixed

This is necessary because changing one product affects all related products too.

Parameters
$post_id : int

The product ID updated/created.

Tags
since
9.8.0
deprecated
10.1.0

This function is deprecated and will be removed in a future version.

wc_get_product_ids_on_sale()

Function that returns an array containing the IDs of the products that are on sale.

wc_get_product_ids_on_sale() : array<string|int, mixed>
Tags
since
2.0

Function that returns an array containing the IDs of the featured products.

wc_get_featured_product_ids() : array<string|int, mixed>
Tags
since
2.1

wc_product_post_type_link()

Filter to allow product_cat in the permalinks for products.

wc_product_post_type_link(string $permalink, WP_Post $post) : string
Parameters
$permalink : string

The existing permalink URL.

$post : WP_Post

WP_Post object.

wc_product_canonical_redirect()

Ensure that the product_cat value determined in `wc_product_post_type_link` is the canonical value.

wc_product_canonical_redirect() : void

If other values are used in this part of the permalink, it will be redirected.

wc_placeholder_img_src()

Get the placeholder image URL either from media, or use the fallback image.

wc_placeholder_img_src([string $size = 'woocommerce_thumbnail' ]) : string
Parameters
$size : string = 'woocommerce_thumbnail'

Thumbnail size to use.

wc_placeholder_img()

Get the placeholder image.

wc_placeholder_img([string $size = 'woocommerce_thumbnail' ][, string|array<string|int, mixed> $attr = '' ]) : string

Uses wp_get_attachment_image if using an attachment ID @since 3.6.0 to handle responsiveness.

Parameters
$size : string = 'woocommerce_thumbnail'

Image size.

$attr : string|array<string|int, mixed> = ''

Optional. Attributes for the image markup. Default empty.

wc_get_formatted_variation()

Variation Formatting.

wc_get_formatted_variation(array<string|int, mixed>|WC_Product_Variation $variation[, bool $flat = false ][, bool $include_names = true ][, bool $skip_attributes_in_name = false ]) : string

Gets a formatted version of variation data or item meta.

Parameters
$variation : array<string|int, mixed>|WC_Product_Variation

Variation object.

$flat : bool = false

Should this be a flat list or HTML list? (default: false).

$include_names : bool = true

include attribute names/labels in the list.

$skip_attributes_in_name : bool = false

Do not list attributes already part of the variation name.

wc_schedule_product_sale_events()

Schedule start/end sale actions for a product based on its sale dates.

wc_schedule_product_sale_events(WC_Product $product) : void

Uses Action Scheduler to fire events at the exact sale start/end times, rather than relying on the daily cron.

An action is not scheduled if an identical one (same hook, args, group and timestamp) is already pending, so concurrent processes saving the same product don't pile up duplicate actions.

Parameters
$product : WC_Product

Product object.

Tags
since
10.5.0

wc_apply_sale_state_for_product()

Apply the expected sale state for a product.

wc_apply_sale_state_for_product(WC_Product $product, string $mode) : void

This is a shared helper used by both the per-product Action Scheduler callbacks and the daily cron safety net.

Parameters
$product : WC_Product

Product object.

$mode : string

'start' or 'end'.

Tags
since
10.5.0

wc_handle_product_start_scheduled_sale()

Handle scheduled sale start for a product.

wc_handle_product_start_scheduled_sale(int $product_id) : void

This is the Action Scheduler callback that fires at the exact sale start time.

Parameters
$product_id : int

Product ID.

Tags
since
10.5.0

wc_handle_product_end_scheduled_sale()

Handle scheduled sale end for a product.

wc_handle_product_end_scheduled_sale(int $product_id) : void

This is the Action Scheduler callback that fires at the exact sale end time.

Parameters
$product_id : int

Product ID.

Tags
since
10.5.0

wc_maybe_schedule_product_sale_events()

Schedule sale events when a product is saved with sale dates.

wc_maybe_schedule_product_sale_events(int $product_id[, WC_Product|null $product = null ]) : void
Parameters
$product_id : int

Product ID.

$product : WC_Product|null = null

Product object (optional).

Tags
since
10.5.0

wc_maybe_schedule_sale_events_on_meta_change()

Schedule sale events when sale date meta is added, updated, or deleted.

wc_maybe_schedule_sale_events_on_meta_change(int|array<string|int, int> $meta_id, int $object_id, string $meta_key) : void

Hooks into post meta operations so per-product sale events are kept in sync regardless of how the meta is written: WooCommerce CRUD, direct update_post_meta() calls from importers, ERP sync tools, or custom code.

Parameters
$meta_id : int|array<string|int, int>

Meta ID (or array of IDs for delete).

$object_id : int

Post ID.

$meta_key : string

Meta key.

Tags
since
10.8.0

wc_scheduled_sales()

Function which handles the start and end of scheduled sales via cron.

wc_scheduled_sales() : mixed

Previously, this daily cron was the only mechanism for starting/ending scheduled sales, which caused timing issues - sales could be "a day off" depending on when WP-Cron ran. Now, per-product Action Scheduler events fire at exact sale times.

This function now acts as a safety net to:

  1. Catch any products missed by the per-product Action Scheduler events
  2. Handle products created before the AS events were introduced

This function is kept for backwards compatibility. Extenders may hook into the woocommerce_scheduled_sales cron event or the before/after hooks fired within.

Note: The before/after hooks (wc_before_products_starting_sales, etc.) only fire when this cron finds products to process. If per-product AS events handled sales on time, these hooks may not fire.

Tags
since
3.0.0

wc_get_attachment_image_attributes()

Get attachment image attributes.

wc_get_attachment_image_attributes(array<string|int, mixed> $attr) : array<string|int, mixed>
Parameters
$attr : array<string|int, mixed>

Image attributes.

wc_prepare_attachment_for_js()

Prepare attachment for JavaScript.

wc_prepare_attachment_for_js(array<string|int, mixed> $response) : array<string|int, mixed>
Parameters
$response : array<string|int, mixed>

JS version of a attachment post object.

wc_product_has_unique_sku()

Check if product sku is unique.

wc_product_has_unique_sku(int $product_id, string $sku) : bool
Parameters
$product_id : int

Product ID.

$sku : string

Product SKU.

Tags
since
2.2

wc_product_has_global_unique_id()

Check if product unique ID is unique.

wc_product_has_global_unique_id(int $product_id, string $global_unique_id) : bool
Parameters
$product_id : int

Product ID.

$global_unique_id : string

Product Unique ID.

Tags
since
9.1.0

wc_product_force_unique_sku()

Force a unique SKU.

wc_product_force_unique_sku(int $product_id) : mixed
Parameters
$product_id : int

Product ID.

Tags
since
3.0.0

wc_product_generate_unique_sku()

Recursively appends a suffix until a unique SKU is found.

wc_product_generate_unique_sku(int $product_id, string $sku, int $index) : string
Parameters
$product_id : int

Product ID.

$sku : string

Product SKU.

$index : int

An optional index that can be added to the product SKU.

Tags
since
3.0.0

wc_get_product_id_by_sku()

Get product ID by SKU.

wc_get_product_id_by_sku(string $sku) : int
Parameters
$sku : string

Product SKU.

Tags
since
2.3.0

wc_get_product_id_by_global_unique_id()

Get product ID by Unique ID.

wc_get_product_id_by_global_unique_id(string $global_unique_id) : int|null
Parameters
$global_unique_id : string

Product Unique ID.

Tags
since
9.1.0

wc_get_product_variation_attributes()

Get attributes/data for an individual variation from the database and maintain its integrity.

wc_get_product_variation_attributes(int $variation_id) : array<string|int, mixed>
Parameters
$variation_id : int

Variation ID.

Tags
since
2.4.0

wc_get_product_cat_ids()

Get all product cats for a product by ID, including hierarchy

wc_get_product_cat_ids(int $product_id) : array<string|int, mixed>
Parameters
$product_id : int

Product ID.

Tags
since
2.5.0

wc_get_product_attachment_props()

Gets data about an attachment, such as alt text and captions.

wc_get_product_attachment_props([int|null $attachment_id = null ][, WC_Product|bool $product = false ]) : array<string|int, mixed>
Parameters
$attachment_id : int|null = null

Attachment ID.

$product : WC_Product|bool = false

WC_Product object.

Tags
since
2.6.0

wc_get_product_visibility_options()

Get product visibility options.

wc_get_product_visibility_options() : array<string|int, mixed>
Tags
since
3.0.0

wc_get_product_tax_class_options()

Get product tax class options.

wc_get_product_tax_class_options() : array<string|int, mixed>
Tags
since
3.0.0

wc_get_product_stock_status_options()

Get stock status options.

wc_get_product_stock_status_options() : array<string|int, mixed>
Tags
since
3.0.0

wc_get_product_backorder_options()

Get backorder options.

wc_get_product_backorder_options() : array<string|int, mixed>
Tags
since
3.0.0

Get related products based on product category and tags.

wc_get_related_products(int $product_id[, int $limit = 5 ][, array<string|int, mixed> $exclude_ids = array() ][, array<string|int, mixed> $related_by = array() ]) : array<string|int, mixed>
Parameters
$product_id : int

Product ID.

$limit : int = 5

Limit of results.

$exclude_ids : array<string|int, mixed> = array()

Exclude IDs from the results.

$related_by : array<string|int, mixed> = array()

Related by category and tags boolean flags.

Tags
since
3.0.0

wc_get_product_term_ids()

Retrieves product term ids for a taxonomy.

wc_get_product_term_ids(int $product_id, string $taxonomy) : array<string|int, mixed>
Parameters
$product_id : int

Product ID.

$taxonomy : string

Taxonomy slug.

Tags
since
3.0.0

wc_get_price_including_tax()

For a given product, and optionally price/qty, work out the price with tax included, based on store settings.

wc_get_price_including_tax(WC_Product $product[, array<string|int, mixed> $args = array() ]) : float|string
Parameters
$product : WC_Product

WC_Product object.

$args : array<string|int, mixed> = array()

Optional arguments to pass product quantity and price.

Tags
since
3.0.0

wc_get_price_excluding_tax()

For a given product, and optionally price/qty, work out the price with tax excluded, based on store settings.

wc_get_price_excluding_tax(WC_Product $product[, array<string|int, mixed> $args = array() ]) : float|string
Parameters
$product : WC_Product

WC_Product object.

$args : array<string|int, mixed> = array()

Optional arguments to pass product quantity and price.

Tags
since
3.0.0

wc_get_price_to_display()

Returns the price including or excluding tax.

wc_get_price_to_display(WC_Product $product[, array<string|int, mixed> $args = array() ]) : float

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.

Parameters
$product : WC_Product

WC_Product object.

$args : array<string|int, mixed> = array()

Optional arguments to pass product quantity and price.

Tags
since
3.0.0
since
7.6.0

Added display_context argument.

wc_get_product_category_list()

Returns the product categories in a list.

wc_get_product_category_list(int $product_id[, string $sep = ', ' ][, string $before = '' ][, string $after = '' ]) : string
Parameters
$product_id : int

Product ID.

$sep : string = ', '

(default: ', ').

$before : string = ''

(default: '').

$after : string = ''

(default: '').

wc_get_product_tag_list()

Returns the product tags in a list.

wc_get_product_tag_list(int $product_id[, string $sep = ', ' ][, string $before = '' ][, string $after = '' ]) : string
Parameters
$product_id : int

Product ID.

$sep : string = ', '

(default: ', ').

$before : string = ''

(default: '').

$after : string = ''

(default: '').

wc_products_array_filter_visible_grouped()

Callback for array filter to get visible grouped products only.

wc_products_array_filter_visible_grouped(WC_Product $product) : bool
Parameters
$product : WC_Product

WC_Product object.

Tags
since
3.1.0

wc_products_array_filter_editable()

Callback for array filter to get products the user can edit only.

wc_products_array_filter_editable(WC_Product $product) : bool
Parameters
$product : WC_Product

WC_Product object.

Tags
since
3.0.0

wc_products_array_filter_readable()

Callback for array filter to get products the user can view only.

wc_products_array_filter_readable(WC_Product $product) : bool
Parameters
$product : WC_Product

WC_Product object.

Tags
since
3.4.0

wc_products_array_orderby()

Sort an array of products by a value.

wc_products_array_orderby(array<string|int, mixed> $products[, string $orderby = 'date' ][, string $order = 'desc' ]) : array<string|int, mixed>
Parameters
$products : array<string|int, mixed>

List of products to be ordered.

$orderby : string = 'date'

Optional order criteria.

$order : string = 'desc'

Ascending or descending order.

Tags
since
3.0.0

wc_deferred_product_sync()

Queue a product for syncing at the end of the request.

wc_deferred_product_sync(int $product_id) : mixed
Parameters
$product_id : int

Product ID.

wc_update_product_lookup_tables_is_running()

See if the lookup table is being generated already.

wc_update_product_lookup_tables_is_running() : bool
Tags
since
3.6.0

wc_update_product_lookup_tables()

Populate lookup table data for products.

wc_update_product_lookup_tables() : mixed
Tags
since
3.6.0

wc_update_product_lookup_tables_column()

Populate lookup table column data.

wc_update_product_lookup_tables_column(string $column) : mixed
Parameters
$column : string

Column name to set.

Tags
since
3.6.0

wc_update_product_lookup_tables_rating_count()

Populate rating count lookup table data for products.

wc_update_product_lookup_tables_rating_count(array<string|int, mixed> $rows) : mixed
Parameters
$rows : array<string|int, mixed>

Rows of rating counts to update in lookup table.

Tags
since
3.6.0

wc_update_product_lookup_tables_rating_count_batch()

Populate a batch of rating count lookup table data for products.

wc_update_product_lookup_tables_rating_count_batch(array<string|int, mixed> $offset, array<string|int, mixed> $limit) : mixed
Parameters
$offset : array<string|int, mixed>

Offset to query.

$limit : array<string|int, mixed>

Limit to query.

Tags
since
3.6.2

Attach product featured image. Use image filename to match a product sku when product is not provided.

wc_product_attach_featured_image(int $attachment_id[, WC_Product $product = null ][, bool $save_product = true ]) : void
Parameters
$attachment_id : int

Media attachment ID.

$product : WC_Product = null

Optional product object.

$save_product : bool = true

If true, the changes in the product will be saved before the method returns.

Tags
since
8.5.0

Source code

<?php
/**
 * WooCommerce Product Functions
 *
 * Functions for product specific things.
 *
 * @package WooCommerce\Functions
 * @version 3.0.0
 */

use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Enums\ProductStockStatus;
use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\Enums\CatalogVisibility;
use Automattic\WooCommerce\Enums\TaxDisplayMode;
use Automattic\WooCommerce\Internal\Caches\ProductTransientsDeferrer;
use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\NumberUtil;
use Automattic\WooCommerce\Internal\ProductImage\MatchImageBySKU;

defined( 'ABSPATH' ) || exit;

/**
 * Standard way of retrieving products based on certain parameters.
 *
 * This function should be used for product retrieval so that we have a data agnostic
 * way to get a list of products.
 *
 * Args and usage: https://developer.woocommerce.com/docs/extensions/core-concepts/wc-get-products/
 *
 * @since  3.0.0
 * @param  array $args Array of args (above).
 * @return array|stdClass Number of pages and an array of product objects if
 *                             paginate is true, or just an array of values.
 */
function wc_get_products( $args ) {
	// Handle some BW compatibility arg names where wp_query args differ in naming.
	$map_legacy = array(
		'numberposts'    => 'limit',
		'post_status'    => 'status',
		'post_parent'    => 'parent',
		'posts_per_page' => 'limit',
		'paged'          => 'page',
	);

	foreach ( $map_legacy as $from => $to ) {
		if ( isset( $args[ $from ] ) ) {
			$args[ $to ] = $args[ $from ];
		}
	}

	$query = new WC_Product_Query( $args );
	return $query->get_products();
}

/**
 * Main function for returning products, uses the WC_Product_Factory class.
 *
 * This function should only be called after 'init' action is finished, as there might be taxonomies that are getting
 * registered during the init action.
 *
 * @since 2.2.0
 *
 * @param mixed $the_product Post object or post ID of the product.
 * @param array $deprecated Previously used to pass arguments to the factory, e.g. to force a type.
 * @return WC_Product|null|false
 */
function wc_get_product( $the_product = false, $deprecated = array() ) {
	if ( ! did_action( 'woocommerce_init' ) || ! did_action( 'woocommerce_after_register_taxonomy' ) || ! did_action( 'woocommerce_after_register_post_type' ) ) {
		/* translators: 1: wc_get_product 2: woocommerce_init 3: woocommerce_after_register_taxonomy 4: woocommerce_after_register_post_type */
		wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s, %3$s and %4$s actions have finished.', 'woocommerce' ), 'wc_get_product', 'woocommerce_init', 'woocommerce_after_register_taxonomy', 'woocommerce_after_register_post_type' ), '3.9' );
		return false;
	}
	if ( ! empty( $deprecated ) ) {
		wc_deprecated_argument( 'args', '3.0', 'Passing args to wc_get_product is deprecated. If you need to force a type, construct the product class directly.' );
	}
	return WC()->product_factory->get_product( $the_product, $deprecated );
}

/**
 * Get a product object.
 *
 * @see WC_Product_Factory::get_product_classname
 * @since 3.9.0
 * @param string $product_type Product type. If used an invalid type a WC_Product_Simple instance will be returned.
 * @param int    $product_id   Product ID.
 * @return WC_Product
 */
function wc_get_product_object( $product_type, $product_id = 0 ) {
	$classname = WC_Product_Factory::get_product_classname( $product_id, $product_type );

	return new $classname( $product_id );
}

/**
 * Returns whether or not SKUS are enabled.
 *
 * @return bool
 */
function wc_product_sku_enabled() {
	return apply_filters( 'wc_product_sku_enabled', true );
}

/**
 * Returns whether or not product weights are enabled.
 *
 * @return bool
 */
function wc_product_weight_enabled() {
	return apply_filters( 'wc_product_weight_enabled', true );
}

/**
 * Returns whether or not product dimensions (HxWxD) are enabled.
 *
 * @return bool
 */
function wc_product_dimensions_enabled() {
	return apply_filters( 'wc_product_dimensions_enabled', true );
}

/**
 * Clear transient cache for product data.
 *
 * @param int $post_id (default: 0) The product ID.
 */
function wc_delete_product_transients( $post_id = 0 ) {
	$container = wc_get_container();

	if ( $container->get( ProductTransientsDeferrer::class )->maybe_defer_deletion( absint( $post_id ) ) ) {
		return;
	}

	$container->get( ProductUtil::class )->delete_product_transients_for_products( array( $post_id ) );
}

/**
 * Delete all related products transients when a product is updated/created.
 * This is necessary because changing one product affects all related products too.
 *
 * @since 9.8.0
 * @deprecated 10.1.0 This function is deprecated and will be removed in a future version.
 * @param int $post_id The product ID updated/created.
 */
function wc_delete_related_product_transients( $post_id ) {
	wc_deprecated_function( 'wc_delete_related_product_transients', '10.1.0', 'This function is deprecated and will be removed in a future version.' );

	if ( ! is_numeric( $post_id ) ) {
		return;
	}

	$transient_name          = 'wc_related_' . $post_id;
	$old_transient           = get_transient( $transient_name );
	$old_related_product_ids = array();

	if ( is_array( $old_transient ) && ! empty( $old_transient ) ) {
		$old_related_product_ids = $old_transient[ array_key_first( $old_transient ) ];
	}

	// Delete current product transient so that it can be refreshed below.
	delete_transient( $transient_name );

	// Gets new related products and sets current product transient.
	$new_related_product_ids = wc_get_related_products( $post_id, 1000 );

	// Combine all product IDs that need their transients cleared.
	$related_product_ids = array_unique(
		array_merge(
			$old_related_product_ids,
			$new_related_product_ids
		)
	);

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

	// Create the list of transient names to delete.
	$related_product_transients = array_map(
		function ( $id ) {
			return 'wc_related_' . $id;
		},
		$related_product_ids
	);
	_wc_delete_transients( $related_product_transients );
}

/**
 * Function that returns an array containing the IDs of the products that are on sale.
 *
 * @since 2.0
 * @return array
 */
function wc_get_product_ids_on_sale() {
	// Load from cache.
	$product_ids_on_sale = get_transient( 'wc_products_onsale' );

	// Valid cache found.
	if ( false !== $product_ids_on_sale ) {
		return $product_ids_on_sale;
	}

	$data_store          = WC_Data_Store::load( 'product' );
	$on_sale_products    = $data_store->get_on_sale_products();
	$product_ids_on_sale = wp_parse_id_list( array_merge( wp_list_pluck( $on_sale_products, 'id' ), array_diff( wp_list_pluck( $on_sale_products, 'parent_id' ), array( 0 ) ) ) );

	set_transient( 'wc_products_onsale', $product_ids_on_sale, DAY_IN_SECONDS * 30 );

	return $product_ids_on_sale;
}

/**
 * Function that returns an array containing the IDs of the featured products.
 *
 * @since 2.1
 * @return array
 */
function wc_get_featured_product_ids() {
	// Load from cache.
	$featured_product_ids = get_transient( 'wc_featured_products' );

	// Valid cache found.
	if ( false !== $featured_product_ids ) {
		return $featured_product_ids;
	}

	$data_store           = WC_Data_Store::load( 'product' );
	$featured             = $data_store->get_featured_product_ids();
	$product_ids          = array_keys( $featured );
	$parent_ids           = array_values( array_filter( $featured ) );
	$featured_product_ids = array_unique( array_merge( $product_ids, $parent_ids ) );

	set_transient( 'wc_featured_products', $featured_product_ids, DAY_IN_SECONDS * 30 );

	return $featured_product_ids;
}

/**
 * Filter to allow product_cat in the permalinks for products.
 *
 * @param  string  $permalink The existing permalink URL.
 * @param  WP_Post $post WP_Post object.
 * @return string
 */
function wc_product_post_type_link( $permalink, $post ) {
	// Abort if post is not a product.
	if ( 'product' !== $post->post_type ) {
		return $permalink;
	}

	// Abort early if the placeholder rewrite tag isn't in the generated URL.
	if ( false === strpos( $permalink, '%' ) ) {
		return $permalink;
	}

	// Only process category if the permalink structure uses category placeholders.
	$needs_category = strpos( $permalink, '%category%' ) !== false || strpos( $permalink, '%product_cat%' ) !== false;
	$product_cat    = '';

	if ( $needs_category ) {
		// Get the custom taxonomy terms in use by this post.
		$terms = get_the_terms( $post->ID, 'product_cat' );

		if ( ! empty( $terms ) && ! is_wp_error( $terms ) && is_array( $terms ) ) {
			// Re-index array to ensure sequential keys starting from 0 since filters may remove some keys.
			$terms = array_values( $terms );

			// Find the deepest category (most ancestors) for the permalink.
			$deepest_term      = $terms[0];
			$deepest_ancestors = $deepest_term->parent ? get_ancestors( $deepest_term->term_id, 'product_cat' ) : array();

			foreach ( $terms as $term ) {
				if ( $term->term_id === $deepest_term->term_id ) {
					continue;
				}
				// Skip root categories - they can't be deeper than current.
				if ( ! $term->parent ) {
					continue;
				}
				$ancestors = get_ancestors( $term->term_id, 'product_cat' );
				if ( count( $ancestors ) > count( $deepest_ancestors ) ) {
					$deepest_ancestors = $ancestors;
					$deepest_term      = $term;
				}
			}

			/**
			 * Filter the product category used for the product permalink.
			 *
			 * By default, the deepest category (most ancestors) is selected. Prior to 9.9.0,
			 * categories were sorted by parent term ID descending, then term ID ascending.
			 * This filter allows customization of which category is used in the product permalink.
			 *
			 * @since 2.4.0
			 * @since 9.9.0 Selection algorithm changed to use deepest category instead of sort order.
			 *
			 * @param WP_Term   $deepest_term The selected category term object (deepest category since 9.9.0).
			 * @param WP_Term[] $terms        All category terms assigned to the product.
			 * @param WP_Post   $post         The product post object.
			 */
			$category_object = apply_filters( 'wc_product_post_type_link_product_cat', $deepest_term, $terms, $post );
			$category_object = ! $category_object instanceof WP_Term ? $deepest_term : $category_object;
			$product_cat     = $category_object->slug;

			if ( $category_object->parent ) {
				// Reuse cached ancestors if the filter didn't change the category, otherwise fetch them.
				$ancestors = ( $category_object->term_id === $deepest_term->term_id )
					? $deepest_ancestors
					: get_ancestors( $category_object->term_id, 'product_cat' );
				foreach ( $ancestors as $ancestor ) {
					$ancestor_object = get_term( $ancestor, 'product_cat' );

					/**
					 * Filter whether to use only the top-level parent category in the product permalink.
					 *
					 * When true, only the top-level ancestor category slug is used instead of
					 * the full category hierarchy path (e.g., 'parent' instead of 'parent/child/grandchild').
					 *
					 * @since 2.6.5
					 *
					 * @param bool $use_parent_only Whether to use only the top-level parent category. Default false.
					 */
					if ( apply_filters( 'woocommerce_product_post_type_link_parent_category_only', false ) ) {
						$product_cat = $ancestor_object->slug;
					} else {
						$product_cat = $ancestor_object->slug . '/' . $product_cat;
					}
				}
			}
		} else {
			// If no terms are assigned to this post, use a string instead (can't leave the placeholder there).
			$product_cat = _x( 'uncategorized', 'slug', 'woocommerce' );
		}
	}

	$find = array(
		'%year%',
		'%monthnum%',
		'%day%',
		'%hour%',
		'%minute%',
		'%second%',
		'%post_id%',
		'%category%',
		'%product_cat%',
	);

	$replace = array(
		date_i18n( 'Y', strtotime( $post->post_date ) ),
		date_i18n( 'm', strtotime( $post->post_date ) ),
		date_i18n( 'd', strtotime( $post->post_date ) ),
		date_i18n( 'H', strtotime( $post->post_date ) ),
		date_i18n( 'i', strtotime( $post->post_date ) ),
		date_i18n( 's', strtotime( $post->post_date ) ),
		(string) $post->ID,
		$product_cat,
		$product_cat,
	);

	$permalink = str_replace( $find, $replace, $permalink );

	return $permalink;
}
add_filter( 'post_type_link', 'wc_product_post_type_link', 10, 2 );

/**
 * Ensure that the product_cat value determined in `wc_product_post_type_link` is the canonical value.
 *
 * If other values are used in this part of the permalink, it will be redirected.
 *
 * @return void
 */
function wc_product_canonical_redirect(): void {
	global $wp_rewrite;

	if (
		! did_action( 'woocommerce_init' )
		|| ! is_product()
		|| ! is_a( $wp_rewrite, WP_Rewrite::class )
	) {
		return;
	}

	// In the event we are dealing with ugly permalinks, this will be empty.
	$specified_category_slug = get_query_var( 'product_cat' );
	$specified_category_slug = is_array( $specified_category_slug ) ? '' : urldecode( (string) $specified_category_slug );

	if ( '' === $specified_category_slug ) {
		return;
	}

	// What category slug did we expect? Normally this maps back to the first assigned product_cat
	// term. However, this is filterable so we use the relevant helper function to figure this out.
	$expected_category_slug = wc_product_post_type_link( '%product_cat%', get_post( get_the_ID() ) );
	$expected_category_slug = urldecode( $expected_category_slug );

	if ( $specified_category_slug === $expected_category_slug ) {
		return;
	}

	// phpcs:ignore WordPress.Security.NonceVerification.Recommended
	$query_vars = isset( $_GET ) && is_array( $_GET ) ? $_GET : array();

	wp_safe_redirect( add_query_arg( $query_vars, wc_get_product( get_the_ID() )->get_permalink() ), 301 );
	exit();
}
add_action( 'template_redirect', 'wc_product_canonical_redirect', 5 );

/**
 * Get the placeholder image URL either from media, or use the fallback image.
 *
 * @param string $size Thumbnail size to use.
 * @return string
 */
function wc_placeholder_img_src( $size = 'woocommerce_thumbnail' ) {
	$src               = WC()->plugin_url() . '/assets/images/placeholder.webp';
	$placeholder_image = get_option( 'woocommerce_placeholder_image', 0 );

	if ( ! empty( $placeholder_image ) ) {
		if ( is_numeric( $placeholder_image ) ) {
			$image = wp_get_attachment_image_src( $placeholder_image, $size );

			if ( ! empty( $image[0] ) ) {
				$src = $image[0];
			}
		} else {
			$src = $placeholder_image;
		}
	}

	return apply_filters( 'woocommerce_placeholder_img_src', $src );
}

/**
 * Get the placeholder image.
 *
 * Uses wp_get_attachment_image if using an attachment ID @since 3.6.0 to handle responsiveness.
 *
 * @param string       $size Image size.
 * @param string|array $attr Optional. Attributes for the image markup. Default empty.
 * @return string
 */
function wc_placeholder_img( $size = 'woocommerce_thumbnail', $attr = '' ) {
	$dimensions        = wc_get_image_size( $size );
	$placeholder_image = get_option( 'woocommerce_placeholder_image', 0 );

	$default_attr = array(
		'class' => 'woocommerce-placeholder wp-post-image',
		'alt'   => __( 'Placeholder', 'woocommerce' ),
	);

	$attr = wp_parse_args( $attr, $default_attr );

	if ( wp_attachment_is_image( $placeholder_image ) ) {
		$image_html = wp_get_attachment_image(
			$placeholder_image,
			$size,
			false,
			$attr
		);
	} else {
		$image      = wc_placeholder_img_src( $size );
		$hwstring   = image_hwstring( $dimensions['width'], $dimensions['height'] );
		$attributes = array();

		foreach ( $attr as $name => $value ) {
			$attributes[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"';
		}

		$image_html = '<img src="' . esc_url( $image ) . '" ' . $hwstring . implode( ' ', $attributes ) . '/>';
	}

	return apply_filters( 'woocommerce_placeholder_img', $image_html, $size, $dimensions );
}

/**
 * Variation Formatting.
 *
 * Gets a formatted version of variation data or item meta.
 *
 * @param array|WC_Product_Variation $variation Variation object.
 * @param bool                       $flat Should this be a flat list or HTML list? (default: false).
 * @param bool                       $include_names include attribute names/labels in the list.
 * @param bool                       $skip_attributes_in_name Do not list attributes already part of the variation name.
 * @return string
 */
function wc_get_formatted_variation( $variation, $flat = false, $include_names = true, $skip_attributes_in_name = false ) {
	$return = '';

	if ( is_a( $variation, 'WC_Product_Variation' ) ) {
		$variation_attributes = $variation->get_attributes();
		$product              = $variation;
		$variation_name       = $variation->get_name();
	} else {
		$product        = false;
		$variation_name = '';
		// Remove attribute_ prefix from names.
		$variation_attributes = array();
		if ( is_array( $variation ) ) {
			foreach ( $variation as $key => $value ) {
				$variation_attributes[ str_replace( 'attribute_', '', $key ) ] = $value;
			}
		}
	}

	$list_type = $include_names ? 'dl' : 'ul';

	if ( is_array( $variation_attributes ) && ! empty( $variation_attributes ) ) {

		if ( ! $flat ) {
			$return = '<' . $list_type . ' class="variation">';
		}

		$variation_list = array();

		foreach ( $variation_attributes as $name => $value ) {
			// If this is a term slug, get the term's nice name.
			if ( taxonomy_exists( $name ) ) {
				$term = get_term_by( 'slug', $value, $name );
				if ( ! is_wp_error( $term ) && $term && null !== $term->name && '' !== $term->name ) {
					$value = $term->name;
				}
			}

			// Do not list attributes already part of the variation name.
			if ( '' === $value || ( $skip_attributes_in_name && wc_is_attribute_in_product_name( $value, $variation_name ) ) ) {
				continue;
			}

			if ( $include_names ) {
				if ( $flat ) {
					$variation_list[] = wc_attribute_label( $name, $product ) . ': ' . rawurldecode( $value );
				} else {
					$variation_list[] = '<dt>' . wc_attribute_label( $name, $product ) . ':</dt><dd>' . rawurldecode( $value ) . '</dd>';
				}
			} elseif ( $flat ) {
					$variation_list[] = rawurldecode( $value );
			} else {
				$variation_list[] = '<li>' . rawurldecode( $value ) . '</li>';
			}
		}

		if ( $flat ) {
			$return .= implode( ', ', $variation_list );
		} else {
			$return .= implode( '', $variation_list );
		}

		if ( ! $flat ) {
			$return .= '</' . $list_type . '>';
		}
	}
	return $return;
}

/**
 * Schedule start/end sale actions for a product based on its sale dates.
 *
 * Uses Action Scheduler to fire events at the exact sale start/end times,
 * rather than relying on the daily cron.
 *
 * An action is not scheduled if an identical one (same hook, args, group and
 * timestamp) is already pending, so concurrent processes saving the same
 * product don't pile up duplicate actions.
 *
 * @since 10.5.0
 * @param WC_Product $product Product object.
 * @return void
 */
function wc_schedule_product_sale_events( WC_Product $product ): void {
	$product_id = $product->get_id();

	$schedule = function ( ?WC_DateTime $date, string $hook ) use ( $product_id ): void {
		if ( is_null( $date ) ) {
			return;
		}

		$timestamp = $date->getTimestamp();
		if ( $timestamp <= time() ) {
			return;
		}

		$args = array( 'product_id' => $product_id );

		// An identical pending action means a concurrent process (parallel save, importer,
		// daily cron) already scheduled it after the unschedule-all step ran. The query
		// filters by the exact timestamp: a pending action for a different time (e.g. left
		// behind by a process that saw older sale dates) must not block scheduling.
		$identical_pending = as_get_scheduled_actions(
			array(
				'hook'         => $hook,
				'args'         => $args,
				'group'        => 'woocommerce-sales',
				'status'       => ActionScheduler_Store::STATUS_PENDING,
				'date'         => gmdate( 'Y-m-d H:i:s', $timestamp ),
				'date_compare' => '=',
				'per_page'     => 1,
			),
			'ids'
		);

		if ( empty( $identical_pending ) ) {
			as_schedule_single_action( $timestamp, $hook, $args, 'woocommerce-sales' );
		}
	};

	$schedule( $product->get_date_on_sale_from( 'edit' ), 'wc_product_start_scheduled_sale' );
	$schedule( $product->get_date_on_sale_to( 'edit' ), 'wc_product_end_scheduled_sale' );
}

/**
 * Apply the expected sale state for a product.
 *
 * This is a shared helper used by both the per-product Action Scheduler
 * callbacks and the daily cron safety net.
 *
 * @since 10.5.0
 * @param WC_Product $product Product object.
 * @param string     $mode    'start' or 'end'.
 * @return void
 */
function wc_apply_sale_state_for_product( WC_Product $product, string $mode ): void {
	$product_id = $product->get_id();

	if ( 'start' === $mode ) {
		$sale_price = $product->get_sale_price( 'edit' );
		if ( $sale_price ) {
			$product->set_price( $sale_price );
			$product->save();

			// Workaround: `_price` is not in `meta_key_to_props` mapping and only syncs
			// when date/price props change in `handle_updated_props()`. Since we only
			// changed `price` prop, we must update `_price` meta directly.
			// See comment in `WC_Product_Data_Store_CPT::handle_updated_props()`.
			update_post_meta( $product_id, '_price', $sale_price );
		}
	} elseif ( 'end' === $mode ) {
		$regular_price = $product->get_regular_price( 'edit' );
		$product->set_price( $regular_price );

		$product->save();

		// Workaround: see above.
		update_post_meta( $product_id, '_price', $regular_price );
	}

	// Refresh the lookup table since only the `price` prop changed, which is
	// not in the tracked props list in handle_updated_props().
	$data_store = WC_Data_Store::load( 'product' );
	if ( $data_store->has_callable( 'refresh_product_lookup_table' ) ) {
		$data_store->refresh_product_lookup_table( $product_id ); // @phpstan-ignore method.notFound (Guarded by has_callable() and called via __call() on the underlying product data store instance.)
	}

	wc_delete_product_transients( $product_id );

	// Sync parent variable product price range if this is a variation.
	if ( $product->is_type( 'variation' ) ) {
		$parent_id = $product->get_parent_id();
		if ( $parent_id ) {
			WC_Product_Variable::sync( $parent_id );
		}
	}
}

/**
 * Handle scheduled sale start for a product.
 *
 * This is the Action Scheduler callback that fires at the exact sale start time.
 *
 * @since 10.5.0
 * @param int $product_id Product ID.
 * @return void
 */
function wc_handle_product_start_scheduled_sale( $product_id ): void {
	$product = wc_get_product( $product_id );
	if ( ! $product ) {
		return;
	}

	// Skip product types with derived prices.
	if ( $product->is_type( array( 'variable', 'grouped' ) ) ) {
		return;
	}

	// Verify sale should still start (dates/price might have changed since scheduling).
	if ( ! $product->get_sale_price( 'edit' ) ) {
		return;
	}

	$now       = time();
	$date_from = $product->get_date_on_sale_from( 'edit' );
	$date_to   = $product->get_date_on_sale_to( 'edit' );

	if ( $date_from && $date_from->getTimestamp() > $now ) {
		return;
	}

	if ( $date_to && $date_to->getTimestamp() < $now ) {
		return;
	}

	if ( (float) $product->get_price( 'edit' ) === (float) $product->get_sale_price( 'edit' ) ) {
		return;
	}

	wc_apply_sale_state_for_product( $product, 'start' );
}
add_action( 'wc_product_start_scheduled_sale', 'wc_handle_product_start_scheduled_sale' );

/**
 * Handle scheduled sale end for a product.
 *
 * This is the Action Scheduler callback that fires at the exact sale end time.
 *
 * @since 10.5.0
 * @param int $product_id Product ID.
 * @return void
 */
function wc_handle_product_end_scheduled_sale( $product_id ): void {
	$product = wc_get_product( $product_id );
	if ( ! $product ) {
		return;
	}

	// Skip product types with derived prices.
	if ( $product->is_type( array( 'variable', 'grouped' ) ) ) {
		return;
	}

	$now     = time();
	$date_to = $product->get_date_on_sale_to( 'edit' );

	if ( $date_to && $date_to->getTimestamp() > $now ) {
		return;
	}

	if ( (float) $product->get_price( 'edit' ) === (float) $product->get_regular_price( 'edit' ) ) {
		return;
	}

	wc_apply_sale_state_for_product( $product, 'end' );
}
add_action( 'wc_product_end_scheduled_sale', 'wc_handle_product_end_scheduled_sale' );

/**
 * Schedule sale events when a product is saved with sale dates.
 *
 * @since 10.5.0
 * @param int             $product_id Product ID.
 * @param WC_Product|null $product    Product object (optional).
 * @return void
 */
function wc_maybe_schedule_product_sale_events( $product_id, $product = null ): void {
	if ( ! $product ) {
		$product = wc_get_product( $product_id );
		if ( ! $product ) {
			return;
		}
	}

	$product_id = $product->get_id();

	// Always clear existing events first.
	as_unschedule_all_actions( 'wc_product_start_scheduled_sale', array( 'product_id' => $product_id ), 'woocommerce-sales' );
	as_unschedule_all_actions( 'wc_product_end_scheduled_sale', array( 'product_id' => $product_id ), 'woocommerce-sales' );

	$date_from = $product->get_date_on_sale_from( 'edit' );
	$date_to   = $product->get_date_on_sale_to( 'edit' );

	if ( $date_from || $date_to ) {
		wc_schedule_product_sale_events( $product );
	}
}

/**
 * Schedule sale events when sale date meta is added, updated, or deleted.
 *
 * Hooks into post meta operations so per-product sale events are kept in sync regardless
 * of how the meta is written: WooCommerce CRUD, direct update_post_meta() calls from
 * importers, ERP sync tools, or custom code.
 *
 * @since 10.8.0
 * @param int|int[] $meta_id    Meta ID (or array of IDs for delete).
 * @param int       $object_id  Post ID.
 * @param string    $meta_key   Meta key.
 * @return void
 */
function wc_maybe_schedule_sale_events_on_meta_change( $meta_id, $object_id, $meta_key ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
	if ( '_sale_price_dates_from' !== $meta_key && '_sale_price_dates_to' !== $meta_key ) {
		return;
	}

	// Prevent duplicate scheduling when a sale handler's save() rewrites dates already in flight.
	if ( doing_action( 'wc_product_start_scheduled_sale' ) || doing_action( 'wc_product_end_scheduled_sale' ) ) {
		return;
	}

	$post_type = get_post_type( $object_id );
	if ( 'product' !== $post_type && 'product_variation' !== $post_type ) {
		return;
	}

	wc_maybe_schedule_product_sale_events( $object_id );
}
add_action( 'added_post_meta', 'wc_maybe_schedule_sale_events_on_meta_change', 10, 3 );
add_action( 'updated_post_meta', 'wc_maybe_schedule_sale_events_on_meta_change', 10, 3 );
add_action( 'deleted_post_meta', 'wc_maybe_schedule_sale_events_on_meta_change', 10, 3 );

/**
 * Function which handles the start and end of scheduled sales via cron.
 *
 * Previously, this daily cron was the only mechanism for starting/ending scheduled
 * sales, which caused timing issues - sales could be "a day off" depending on when
 * WP-Cron ran. Now, per-product Action Scheduler events fire at exact sale times.
 *
 * This function now acts as a safety net to:
 * 1. Catch any products missed by the per-product Action Scheduler events
 * 2. Handle products created before the AS events were introduced
 *
 * This function is kept for backwards compatibility. Extenders may hook into the
 * `woocommerce_scheduled_sales` cron event or the before/after hooks fired within.
 *
 * Note: The before/after hooks (wc_before_products_starting_sales, etc.) only fire
 * when this cron finds products to process. If per-product AS events handled sales
 * on time, these hooks may not fire.
 *
 * @since 3.0.0
 */
function wc_scheduled_sales() {
	$data_store = WC_Data_Store::load( 'product' );

	$product_util           = wc_get_container()->get( ProductUtil::class );
	$must_refresh_transient = false;

	// Sales which are due to start.
	$product_ids = $data_store->get_starting_sales();
	if ( $product_ids ) {
		_prime_post_caches( $product_ids );
		$must_refresh_transient = true;
		do_action( 'wc_before_products_starting_sales', $product_ids );

		foreach ( $product_ids as $product_id ) {
			$product = wc_get_product( $product_id );

			if ( $product ) {
				wc_apply_sale_state_for_product( $product, 'start' );
				// Note: wc_apply_sale_state_for_product() calls save(), which writes sale
				// date meta and triggers wc_maybe_schedule_sale_events_on_meta_change(),
				// which schedules the end AS event.
			}

			$product_util->delete_product_specific_transients( $product ? $product : $product_id );
		}
		do_action( 'wc_after_products_starting_sales', $product_ids );
		delete_transient( 'wc_products_onsale' );
	}

	// Sales which are due to end.
	$product_ids = $data_store->get_ending_sales();
	if ( $product_ids ) {
		_prime_post_caches( $product_ids );
		$must_refresh_transient = true;
		do_action( 'wc_before_products_ending_sales', $product_ids );

		foreach ( $product_ids as $product_id ) {
			$product = wc_get_product( $product_id );

			if ( $product ) {
				wc_apply_sale_state_for_product( $product, 'end' );
			}

			$product_util->delete_product_specific_transients( $product ? $product : $product_id );
		}
		do_action( 'wc_after_products_ending_sales', $product_ids );
		delete_transient( 'wc_products_onsale' );
	}

	if ( $must_refresh_transient ) {
		// Kept for compatibility, WooCommerce core doesn't use product transient versions anymore.
		WC_Cache_Helper::get_transient_version( 'product', true );
	}
}
add_action( 'woocommerce_scheduled_sales', 'wc_scheduled_sales' );

/**
 * Get attachment image attributes.
 *
 * @param array $attr Image attributes.
 * @return array
 */
function wc_get_attachment_image_attributes( $attr ) {
	/*
	 * If the user can manage woocommerce, allow them to
	 * see the image content.
	 */
	if ( current_user_can( 'manage_woocommerce' ) ) {
		return $attr;
	}

	/*
	 * If the user does not have the right capabilities,
	 * filter out the image source and replace with placeholder
	 * image.
	 */
	if ( isset( $attr['src'] ) && strstr( $attr['src'], 'woocommerce_uploads/' ) ) {
		$attr['src'] = wc_placeholder_img_src();

		if ( isset( $attr['srcset'] ) ) {
			$attr['srcset'] = '';
		}
	}
	return $attr;
}
add_filter( 'wp_get_attachment_image_attributes', 'wc_get_attachment_image_attributes' );


/**
 * Prepare attachment for JavaScript.
 *
 * @param array $response JS version of a attachment post object.
 * @return array
 */
function wc_prepare_attachment_for_js( $response ) {
	/*
	 * If the user can manage woocommerce, allow them to
	 * see the image content.
	 */
	if ( current_user_can( 'manage_woocommerce' ) ) {
		return $response;
	}

	/*
	 * If the user does not have the right capabilities,
	 * filter out the image source and replace with placeholder
	 * image.
	 */
	if ( isset( $response['url'] ) && strstr( $response['url'], 'woocommerce_uploads/' ) ) {
		$response['full']['url'] = wc_placeholder_img_src();
		if ( isset( $response['sizes'] ) ) {
			foreach ( $response['sizes'] as $size => $value ) {
				$response['sizes'][ $size ]['url'] = wc_placeholder_img_src();
			}
		}
	}

	return $response;
}
add_filter( 'wp_prepare_attachment_for_js', 'wc_prepare_attachment_for_js' );

/**
 * Track product views.
 */
function wc_track_product_view() {
	if ( ! is_singular( 'product' ) || ! is_active_widget( false, false, 'woocommerce_recently_viewed_products', true ) ) {
		return;
	}

	global $post;

	if ( empty( $_COOKIE['woocommerce_recently_viewed'] ) ) { // @codingStandardsIgnoreLine.
		$viewed_products = array();
	} else {
		$viewed_products = wp_parse_id_list( (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) ); // @codingStandardsIgnoreLine.
	}

	// Unset if already in viewed products list.
	$keys = array_flip( $viewed_products );

	if ( isset( $keys[ $post->ID ] ) ) {
		unset( $viewed_products[ $keys[ $post->ID ] ] );
	}

	$viewed_products[] = $post->ID;

	if ( count( $viewed_products ) > 15 ) {
		array_shift( $viewed_products );
	}

	// Store for session only.
	wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $viewed_products ) );
}

add_action( 'template_redirect', 'wc_track_product_view', 20 );

/**
 * Get product types.
 *
 * @since 2.2
 * @return array
 */
function wc_get_product_types() {
	return (array) apply_filters(
		'product_type_selector',
		array(
			ProductType::SIMPLE   => __( 'Simple product', 'woocommerce' ),
			ProductType::GROUPED  => __( 'Grouped product', 'woocommerce' ),
			ProductType::EXTERNAL => __( 'External/Affiliate product', 'woocommerce' ),
			ProductType::VARIABLE => __( 'Variable product', 'woocommerce' ),
		)
	);
}

/**
 * Check if product sku is unique.
 *
 * @since 2.2
 * @param int    $product_id Product ID.
 * @param string $sku Product SKU.
 * @return bool
 */
function wc_product_has_unique_sku( $product_id, $sku ) {
	/**
	 * Gives plugins an opportunity to verify SKU uniqueness themselves.
	 *
	 * @since 9.0.0
	 *
	 * @param bool|null $has_unique_sku Set to a boolean value to short-circuit the default SKU check.
	 * @param int $product_id The ID of the current product.
	 * @param string $sku The SKU to check for uniqueness.
	 */
	$has_unique_sku = apply_filters( 'wc_product_pre_has_unique_sku', null, $product_id, $sku );
	if ( ! is_null( $has_unique_sku ) ) {
		return boolval( $has_unique_sku );
	}

	$data_store = WC_Data_Store::load( 'product' );
	$sku_found  = $data_store->is_existing_sku( $product_id, $sku );

	if ( apply_filters( 'wc_product_has_unique_sku', $sku_found, $product_id, $sku ) ) {
		return false;
	}

	return true;
}

/**
 * Check if product unique ID is unique.
 *
 * @since 9.1.0
 * @param int    $product_id Product ID.
 * @param string $global_unique_id Product Unique ID.
 * @return bool
 */
function wc_product_has_global_unique_id( $product_id, $global_unique_id ) {
	/**
	 * Gives plugins an opportunity to verify Unique ID uniqueness themselves.
	 *
	 * @since 9.1.0
	 *
	 * @param bool|null $has_global_unique_id Set to a boolean value to short-circuit the default Unique ID check.
	 * @param int $product_id The ID of the current product.
	 * @param string $sku The Unique ID to check for uniqueness.
	 */
	$has_global_unique_id = apply_filters( 'wc_product_pre_has_global_unique_id', null, $product_id, $global_unique_id );
	if ( ! is_null( $has_global_unique_id ) ) {
		return boolval( $has_global_unique_id );
	}

	$data_store = WC_Data_Store::load( 'product' );
	if ( $data_store->has_callable( 'is_existing_global_unique_id' ) ) {
		$global_unique_id_found = $data_store->is_existing_global_unique_id( $product_id, $global_unique_id );
	} else {
		$logger = wc_get_logger();
		$logger->error( 'The method is_existing_global_unique_id is not implemented in the data store.', array( 'source' => 'wc_product_has_global_unique_id' ) );
	}
	/**
	 * Gives plugins an opportunity to verify Unique ID uniqueness themselves.
	 *
	 * @since 9.1.0
	 *
	 * @param boolean $global_unique_id_found Whether the Unique ID is found.
	 * @param int $product_id The ID of the current product.
	 * @param string $sku The Unique ID to check for uniqueness.
	 */
	if ( apply_filters( 'wc_product_has_global_unique_id', $global_unique_id_found, $product_id, $global_unique_id ) ) {
		return false;
	}

	return true;
}

/**
 * Force a unique SKU.
 *
 * @since  3.0.0
 * @param  integer $product_id Product ID.
 */
function wc_product_force_unique_sku( $product_id ) {
	$product     = wc_get_product( $product_id );
	$current_sku = $product ? $product->get_sku( 'edit' ) : '';

	if ( $current_sku ) {
		try {
			$new_sku = wc_product_generate_unique_sku( $product_id, $current_sku );

			if ( $current_sku !== $new_sku ) {
				$product->set_sku( $new_sku );
				$product->save();
			}
		} catch ( Exception $e ) {} // @codingStandardsIgnoreLine.
	}
}

/**
 * Recursively appends a suffix until a unique SKU is found.
 *
 * @since  3.0.0
 * @param  integer $product_id Product ID.
 * @param  string  $sku Product SKU.
 * @param  integer $index An optional index that can be added to the product SKU.
 * @return string
 */
function wc_product_generate_unique_sku( $product_id, $sku, $index = 0 ) {
	$generated_sku = 0 < $index ? $sku . '-' . $index : $sku;

	if ( ! wc_product_has_unique_sku( $product_id, $generated_sku ) ) {
		$generated_sku = wc_product_generate_unique_sku( $product_id, $sku, ( $index + 1 ) );
	}

	return $generated_sku;
}

/**
 * Get product ID by SKU.
 *
 * @since  2.3.0
 * @param  string $sku Product SKU.
 * @return int
 */
function wc_get_product_id_by_sku( $sku ) {
	$data_store = WC_Data_Store::load( 'product' );
	return $data_store->get_product_id_by_sku( $sku );
}

/**
 * Get product ID by Unique ID.
 *
 * @since  9.1.0
 * @param  string $global_unique_id Product Unique ID.
 * @return int|null
 */
function wc_get_product_id_by_global_unique_id( $global_unique_id ) {
	$data_store = WC_Data_Store::load( 'product' );
	if ( $data_store->has_callable( 'get_product_id_by_global_unique_id' ) ) {
		return $data_store->get_product_id_by_global_unique_id( $global_unique_id );
	} else {
		$logger = wc_get_logger();
		$logger->error( 'The method get_product_id_by_global_unique_id is not implemented in the data store.', array( 'source' => 'wc_get_product_id_by_global_unique_id' ) );
	}
	return null;
}

/**
 * Get attributes/data for an individual variation from the database and maintain its integrity.
 *
 * @since  2.4.0
 * @param  int $variation_id Variation ID.
 * @return array
 */
function wc_get_product_variation_attributes( $variation_id ) {
	// Build variation data from meta.
	$all_meta                = is_array( get_post_meta( $variation_id ) ) ? get_post_meta( $variation_id ) : array();
	$parent_id               = wp_get_post_parent_id( $variation_id );
	$parent_attributes       = array_filter( (array) get_post_meta( $parent_id, '_product_attributes', true ) );
	$found_parent_attributes = array();
	$variation_attributes    = array();

	// Compare to parent variable product attributes and ensure they match.
	foreach ( $parent_attributes as $attribute_name => $options ) {
		if ( ! empty( $options['is_variation'] ) ) {
			$attribute                 = 'attribute_' . sanitize_title( $attribute_name );
			$found_parent_attributes[] = $attribute;
			if ( ! array_key_exists( $attribute, $variation_attributes ) ) {
				$variation_attributes[ $attribute ] = ''; // Add it - 'any' will be assumed.
			}
		}
	}

	// Get the variation attributes from meta.
	foreach ( $all_meta as $name => $value ) {
		// Only look at valid attribute meta, and also compare variation level attributes and remove any which do not exist at parent level.
		if ( 0 !== strpos( $name, 'attribute_' ) || ! in_array( $name, $found_parent_attributes, true ) ) {
			unset( $variation_attributes[ $name ] );
			continue;
		}
		/**
		 * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute.
		 * Attempt to get full version of the text attribute from the parent.
		 */
		if ( sanitize_title( $value[0] ) === $value[0] && version_compare( get_post_meta( $parent_id, '_product_version', true ), '2.4.0', '<' ) ) {
			foreach ( $parent_attributes as $attribute ) {
				if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) {
					continue;
				}
				$text_attributes = wc_get_text_attributes( $attribute['value'] );

				foreach ( $text_attributes as $text_attribute ) {
					if ( sanitize_title( $text_attribute ) === $value[0] ) {
						$value[0] = $text_attribute;
						break;
					}
				}
			}
		}

		$variation_attributes[ $name ] = $value[0];
	}

	return $variation_attributes;
}

/**
 * Get all product cats for a product by ID, including hierarchy
 *
 * @since  2.5.0
 * @param  int $product_id Product ID.
 * @return array
 */
function wc_get_product_cat_ids( $product_id ) {
	$product_cats = wc_get_product_term_ids( $product_id, 'product_cat' );

	foreach ( $product_cats as $product_cat ) {
		$product_cats = array_merge( $product_cats, get_ancestors( $product_cat, 'product_cat' ) );
	}

	return $product_cats;
}

/**
 * Gets data about an attachment, such as alt text and captions.
 *
 * @since 2.6.0
 *
 * @param int|null        $attachment_id Attachment ID.
 * @param WC_Product|bool $product WC_Product object.
 *
 * @return array
 */
function wc_get_product_attachment_props( $attachment_id = null, $product = false ) {
	$props      = array(
		'title'   => '',
		'caption' => '',
		'url'     => '',
		'alt'     => '',
		'src'     => '',
		'srcset'  => false,
		'sizes'   => false,
	);
	$attachment = get_post( $attachment_id );

	if ( $attachment && 'attachment' === $attachment->post_type ) {
		$props['title']   = wp_strip_all_tags( $attachment->post_title );
		$props['caption'] = wp_strip_all_tags( $attachment->post_excerpt );
		$props['url']     = wp_get_attachment_url( $attachment_id );

		// Alt text.
		$alt_text = array( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ), $props['caption'], wp_strip_all_tags( $attachment->post_title ) );

		if ( $product && $product instanceof WC_Product ) {
			$alt_text[] = wp_strip_all_tags( get_the_title( $product->get_id() ) );
		}

		$alt_text     = array_filter( $alt_text );
		$props['alt'] = $alt_text ? reset( $alt_text ) : '';

		/**
		 * Filters the size for the full gallery image.
		 *
		 * @param string $size Image size name.
		 *
		 * @since 2.6.0
		 */
		$full_size           = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) );
		$src                 = wp_get_attachment_image_src( $attachment_id, $full_size );
		$props['full_src']   = $src[0] ?? null;
		$props['full_src_w'] = $src[1] ?? null;
		$props['full_src_h'] = $src[2] ?? null;

		$gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' );
		/**
		 * Filters the size for the gallery thumbnail.
		 *
		 * @param array $size Array containing width and height dimensions.
		 *
		 * @since 2.6.0
		 */
		$gallery_thumbnail_size           = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) );
		$src                              = wp_get_attachment_image_src( $attachment_id, $gallery_thumbnail_size );
		$props['gallery_thumbnail_src']   = $src[0] ?? null;
		$props['gallery_thumbnail_src_w'] = $src[1] ?? null;
		$props['gallery_thumbnail_src_h'] = $src[2] ?? null;

		/**
		 * Filters the thumbnail size.
		 *
		 * @param string $size Image size name.
		 *
		 * @since 2.6.0
		 */
		$thumbnail_size       = apply_filters( 'woocommerce_thumbnail_size', 'woocommerce_thumbnail' );
		$src                  = wp_get_attachment_image_src( $attachment_id, $thumbnail_size );
		$props['thumb_src']   = $src[0] ?? null;
		$props['thumb_src_w'] = $src[1] ?? null;
		$props['thumb_src_h'] = $src[2] ?? null;

		/**
		 * Filters the size for the gallery image.
		 *
		 * @param string $size Image size name.
		 *
		 * @since 2.6.0
		 */
		$image_size      = apply_filters( 'woocommerce_gallery_image_size', 'woocommerce_single' );
		$src             = wp_get_attachment_image_src( $attachment_id, $image_size );
		$props['src']    = $src[0] ?? null;
		$props['src_w']  = $src[1] ?? null;
		$props['src_h']  = $src[2] ?? null;
		$props['srcset'] = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, $image_size ) : false;
		$props['sizes']  = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, $image_size ) : false;
	}
	return $props;
}

/**
 * Get product visibility options.
 *
 * @since 3.0.0
 * @return array
 */
function wc_get_product_visibility_options() {
	return apply_filters(
		'woocommerce_product_visibility_options',
		array(
			CatalogVisibility::VISIBLE => __( 'Shop and search results', 'woocommerce' ),
			CatalogVisibility::CATALOG => __( 'Shop only', 'woocommerce' ),
			CatalogVisibility::SEARCH  => __( 'Search results only', 'woocommerce' ),
			CatalogVisibility::HIDDEN  => __( 'Hidden', 'woocommerce' ),
		)
	);
}

/**
 * Get product tax class options.
 *
 * @since 3.0.0
 * @return array
 */
function wc_get_product_tax_class_options() {
	$tax_classes           = WC_Tax::get_tax_classes();
	$tax_class_options     = array();
	$tax_class_options[''] = __( 'Standard', 'woocommerce' );

	if ( ! empty( $tax_classes ) ) {
		foreach ( $tax_classes as $class ) {
			$tax_class_options[ sanitize_title( $class ) ] = $class;
		}
	}
	return $tax_class_options;
}

/**
 * Get stock status options.
 *
 * @since 3.0.0
 * @return array
 */
function wc_get_product_stock_status_options() {
	return apply_filters(
		'woocommerce_product_stock_status_options',
		array(
			ProductStockStatus::IN_STOCK     => __( 'In stock', 'woocommerce' ),
			ProductStockStatus::OUT_OF_STOCK => __( 'Out of stock', 'woocommerce' ),
			ProductStockStatus::ON_BACKORDER => __( 'On backorder', 'woocommerce' ),
		)
	);
}

/**
 * Get backorder options.
 *
 * @since 3.0.0
 * @return array
 */
function wc_get_product_backorder_options() {
	return array(
		'no'     => __( 'Do not allow', 'woocommerce' ),
		'notify' => __( 'Allow, but notify customer', 'woocommerce' ),
		'yes'    => __( 'Allow', 'woocommerce' ),
	);
}

/**
 * Get related products based on product category and tags.
 *
 * @since  3.0.0
 * @param  int   $product_id  Product ID.
 * @param  int   $limit       Limit of results.
 * @param  array $exclude_ids Exclude IDs from the results.
 * @param  array $related_by  Related by category and tags boolean flags.
 * @return array
 */
function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array(), $related_by = array() ) {
	// Log an error if the limit is not an integer since this is what we expect.
	// However this is not a problem and we can continue.
	if ( ! is_int( $limit ) ) {
		wc_get_logger()->error(
			sprintf(
				'Invalid limit type passed to wc_get_related_products. Expected integer, got %s with value: %s',
				gettype( $limit ),
				wp_json_encode( $limit )
			),
			array( 'source' => 'wc_get_related_products' )
		);
	}

	// If the limit is not numeric, set it to null.
	$limit = is_numeric( $limit ) ? (int) $limit : null;

	if ( null === $limit ) {
		return array();
	}

	$product_id     = absint( $product_id );
	$limit          = $limit >= -1 ? $limit : 5;
	$exclude_ids    = array_merge( array( 0, $product_id ), $exclude_ids );
	$transient_name = 'wc_related_' . $product_id;
	$query_args     = http_build_query(
		array(
			'limit'       => $limit,
			'exclude_ids' => $exclude_ids,
			'related_by'  => $related_by,
		)
	);

	$transient     = get_transient( $transient_name );
	$related_posts = $transient && is_array( $transient ) && isset( $transient[ $query_args ] ) ? $transient[ $query_args ] : false;

	// We want to query related posts if they are not cached, or we don't have enough.
	if ( false === $related_posts || count( $related_posts ) < $limit ) {

		$cats_array = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_cat_terms', wc_get_product_term_ids( $product_id, 'product_cat' ), $product_id ) : array();
		$tags_array = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_tag_terms', wc_get_product_term_ids( $product_id, 'product_tag' ), $product_id ) : array();

		// Don't bother if none are set, unless woocommerce_product_related_posts_force_display is set to true in which case all products are related.
		if ( empty( $cats_array ) && empty( $tags_array ) && ! apply_filters( 'woocommerce_product_related_posts_force_display', false, $product_id ) ) {
			$related_posts = array();
		} else {
			$data_store    = WC_Data_Store::load( 'product' );
			$related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $limit + 10, $product_id );
		}

		if ( $transient && is_array( $transient ) ) {
			$transient[ $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.
 *
 * @param int    $product_id Product ID.
 * @param string $sep (default: ', ').
 * @param string $before (default: '').
 * @param string $after (default: '').
 * @return string
 */
function wc_get_product_category_list( $product_id, $sep = ', ', $before = '', $after = '' ) {
	return get_the_term_list( $product_id, 'product_cat', $before, $sep, $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' ) && ( ProductStatus::PUBLISH === $product->get_status() || current_user_can( 'edit_product', $product->get_id() ) );
}

/**
 * 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' );

	// 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 ( $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'
			);
		}
	}

	// 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;
	}
	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 ) {
		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 ) {
		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' );