/**
* Privacy/GDPR related functionality which ties into WordPress functionality.
*
* @since 3.4.0
* @package WooCommerce\Classes
*/
use Automattic\WooCommerce\Enums\OrderInternalStatus;
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( 'WC_Privacy_Background_Process', false ) ) {
include_once __DIR__ . '/class-wc-privacy-background-process.php';
}
/**
* WC_Privacy Class.
*/
class WC_Privacy extends WC_Abstract_Privacy {
/**
* Background process to clean up orders.
*
* @var WC_Privacy_Background_Process
*/
protected static $background_process;
/**
* Init - hook into events.
*/
public function __construct() {
parent::__construct();
// Initialize data exporters and erasers.
add_action( 'init', array( $this, 'register_erasers_exporters' ) );
// Cleanup orders daily - this is a callback on a daily cron event.
add_action( 'woocommerce_cleanup_personal_data', array( $this, 'queue_cleanup_personal_data' ) );
// Handles custom anonymization types not included in core.
add_filter( 'wp_privacy_anonymize_data', array( $this, 'anonymize_custom_data_types' ), 10, 3 );
// When this is fired, data is removed in a given order. Called from bulk actions.
add_action( 'woocommerce_remove_order_personal_data', array( 'WC_Privacy_Erasers', 'remove_order_personal_data' ) );
}
/**
* Initial registration of privacy erasers and exporters.
*
* Due to the use of translation functions, this should run only after plugins loaded.
*/
public function register_erasers_exporters() {
$this->name = __( 'WooCommerce', 'woocommerce' );
if ( ! self::$background_process ) {
self::$background_process = new WC_Privacy_Background_Process();
}
// Include supporting classes.
include_once __DIR__ . '/class-wc-privacy-erasers.php';
include_once __DIR__ . '/class-wc-privacy-exporters.php';
// This hook registers WooCommerce data exporters.
$this->add_exporter( 'woocommerce-customer-data', __( 'WooCommerce Customer Data', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'customer_data_exporter' ) );
$this->add_exporter( 'woocommerce-customer-orders', __( 'WooCommerce Customer Orders', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'order_data_exporter' ) );
$this->add_exporter( 'woocommerce-customer-downloads', __( 'WooCommerce Customer Downloads', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'download_data_exporter' ) );
$this->add_exporter( 'woocommerce-customer-tokens', __( 'WooCommerce Customer Payment Tokens', 'woocommerce' ), array( 'WC_Privacy_Exporters', 'customer_tokens_exporter' ) );
// This hook registers WooCommerce data erasers.
$this->add_eraser( 'woocommerce-customer-data', __( 'WooCommerce Customer Data', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'customer_data_eraser' ) );
$this->add_eraser( 'woocommerce-customer-orders', __( 'WooCommerce Customer Orders', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'order_data_eraser' ) );
$this->add_eraser( 'woocommerce-customer-downloads', __( 'WooCommerce Customer Downloads', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'download_data_eraser' ) );
$this->add_eraser( 'woocommerce-customer-tokens', __( 'WooCommerce Customer Payment Tokens', 'woocommerce' ), array( 'WC_Privacy_Erasers', 'customer_tokens_eraser' ) );
}
/**
* Add privacy policy content for the privacy policy page.
*
* @since 3.4.0
*/
public function get_privacy_message() {
$content = '
' .
'
' .
__( 'This sample language includes the basics around what personal data your store may be collecting, storing and sharing, as well as who may have access to that data. Depending on what settings are enabled and which additional plugins are used, the specific information shared by your store will vary. We recommend consulting with a lawyer when deciding what information to disclose on your privacy policy.', 'woocommerce' ) .
'
' .
'
' . __( 'We collect information about you during the checkout process on our store.', 'woocommerce' ) . '
' .
'
' . __( 'What we collect and store', 'woocommerce' ) . '
' .
'
' . __( 'While you visit our site, we’ll track:', 'woocommerce' ) . '
' .
'
' .
'- ' . __( 'Products you’ve viewed: we’ll use this to, for example, show you products you’ve recently viewed', 'woocommerce' ) . '
' .
'- ' . __( 'Location, IP address and browser type: we’ll use this for purposes like estimating taxes and shipping', 'woocommerce' ) . '
' .
'- ' . __( 'Shipping address: we’ll ask you to enter this so we can, for instance, estimate shipping before you place an order, and send you the order!', 'woocommerce' ) . '
' .
'
' .
'
' . __( 'We’ll also use cookies to keep track of cart contents while you’re browsing our site.', 'woocommerce' ) . '
' .
'
' .
__( 'Note: you may want to further detail your cookie policy, and link to that section from here.', 'woocommerce' ) .
'
' .
'
' . __( 'When you purchase from us, we’ll ask you to provide information including your name, billing address, shipping address, email address, phone number, credit card/payment details and optional account information like username and password. We’ll use this information for purposes, such as, to:', 'woocommerce' ) . '
' .
'
' .
'- ' . __( 'Send you information about your account and order', 'woocommerce' ) . '
' .
'- ' . __( 'Respond to your requests, including refunds and complaints', 'woocommerce' ) . '
' .
'- ' . __( 'Process payments and prevent fraud', 'woocommerce' ) . '
' .
'- ' . __( 'Set up your account for our store', 'woocommerce' ) . '
' .
'- ' . __( 'Comply with any legal obligations we have, such as calculating taxes', 'woocommerce' ) . '
' .
'- ' . __( 'Improve our store offerings', 'woocommerce' ) . '
' .
'- ' . __( 'Send you marketing messages, if you choose to receive them', 'woocommerce' ) . '
' .
'
' .
'
' . __( 'If you create an account, we will store your name, address, email and phone number, which will be used to populate the checkout for future orders.', 'woocommerce' ) . '
' .
'
' . __( 'We generally store information about you for as long as we need the information for the purposes for which we collect and use it, and we are not legally required to continue to keep it. For example, we will store order information for XXX years for tax and accounting purposes. This includes your name, email address and billing and shipping addresses.', 'woocommerce' ) . '
' .
'
' . __( 'We will also store comments or reviews, if you choose to leave them.', 'woocommerce' ) . '
' .
'
' . __( 'Who on our team has access', 'woocommerce' ) . '
' .
'
' . __( 'Members of our team have access to the information you provide us. For example, both Administrators and Shop Managers can access:', 'woocommerce' ) . '
' .
'
' .
'- ' . __( 'Order information like what was purchased, when it was purchased and where it should be sent, and', 'woocommerce' ) . '
' .
'- ' . __( 'Customer information like your name, email address, and billing and shipping information.', 'woocommerce' ) . '
' .
'
' .
'
' . __( 'Our team members have access to this information to help fulfill orders, process refunds and support you.', 'woocommerce' ) . '
' .
'
' . __( 'What we share with others', 'woocommerce' ) . '
' .
'
' .
__( 'In this section you should list who you’re sharing data with, and for what purpose. This could include, but may not be limited to, analytics, marketing, payment gateways, shipping providers, and third party embeds.', 'woocommerce' ) .
'
' .
'
' . __( 'We share information with third parties who help us provide our orders and store services to you; for example --', 'woocommerce' ) . '
' .
'
' . __( 'Payments', 'woocommerce' ) . '
' .
'
' .
__( 'In this subsection you should list which third party payment processors you’re using to take payments on your store since these may handle customer data. We’ve included PayPal as an example, but you should remove this if you’re not using PayPal.', 'woocommerce' ) .
'
' .
'
' . __( 'We accept payments through PayPal. When processing payments, some of your data will be passed to PayPal, including information required to process or support the payment, such as the purchase total and billing information.', 'woocommerce' ) . '
' .
'
' . __( 'Please see the PayPal Privacy Policy for more details.', 'woocommerce' ) . '
' .
'
';
return apply_filters( 'wc_privacy_policy_content', $content );
}
/**
* Spawn events for order cleanup.
*/
public function queue_cleanup_personal_data() {
self::$background_process->push_to_queue( array( 'task' => 'trash_pending_orders' ) );
self::$background_process->push_to_queue( array( 'task' => 'trash_failed_orders' ) );
self::$background_process->push_to_queue( array( 'task' => 'trash_cancelled_orders' ) );
self::$background_process->push_to_queue( array( 'task' => 'anonymize_refunded_orders' ) );
self::$background_process->push_to_queue( array( 'task' => 'anonymize_completed_orders' ) );
self::$background_process->push_to_queue( array( 'task' => 'delete_inactive_accounts' ) );
self::$background_process->save()->dispatch();
}
/**
* Handle some custom types of data and anonymize them.
*
* @param string $anonymous Anonymized string.
* @param string $type Type of data.
* @param string $data The data being anonymized.
* @return string Anonymized string.
*/
public function anonymize_custom_data_types( $anonymous, $type, $data ) {
switch ( $type ) {
case 'address_state':
case 'address_country':
$anonymous = ''; // Empty string - we don't want to store anything after removal.
break;
case 'phone':
$anonymous = preg_replace( '/\d/u', '0', $data );
break;
case 'numeric_id':
$anonymous = 0;
break;
}
return $anonymous;
}
/**
* Find and trash old orders.
*
* @since 3.4.0
* @param int $limit Limit orders to process per batch.
* @return int Number of orders processed.
*/
public static function trash_pending_orders( $limit = 20 ) {
$option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_pending_orders' ) );
if ( empty( $option['number'] ) ) {
return 0;
}
return self::trash_orders_query(
apply_filters(
'woocommerce_trash_pending_orders_query_args',
array(
'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ),
'limit' => $limit, // Batches of 20.
'status' => OrderInternalStatus::PENDING,
'type' => 'shop_order',
)
)
);
}
/**
* Find and trash old orders.
*
* @since 3.4.0
* @param int $limit Limit orders to process per batch.
* @return int Number of orders processed.
*/
public static function trash_failed_orders( $limit = 20 ) {
$option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_failed_orders' ) );
if ( empty( $option['number'] ) ) {
return 0;
}
return self::trash_orders_query(
apply_filters(
'woocommerce_trash_failed_orders_query_args',
array(
'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ),
'limit' => $limit, // Batches of 20.
'status' => OrderInternalStatus::FAILED,
'type' => 'shop_order',
)
)
);
}
/**
* Find and trash old orders.
*
* @since 3.4.0
* @param int $limit Limit orders to process per batch.
* @return int Number of orders processed.
*/
public static function trash_cancelled_orders( $limit = 20 ) {
$option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_cancelled_orders' ) );
if ( empty( $option['number'] ) ) {
return 0;
}
return self::trash_orders_query(
apply_filters(
'woocommerce_trash_cancelled_orders_query_args',
array(
'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ),
'limit' => $limit, // Batches of 20.
'status' => OrderInternalStatus::CANCELLED,
'type' => 'shop_order',
)
)
);
}
/**
* Find and Anonymize refunded orders.
*
* @since 9.8.0
* @param int $limit Limit orders to process per batch.
* @return int Number of orders processed.
*/
public static function anonymize_refunded_orders( $limit = 20 ) {
$option = wc_parse_relative_date_option( get_option( 'woocommerce_anonymize_refunded_orders' ) );
if ( empty( $option['number'] ) ) {
return 0;
}
return self::anonymize_orders_query(
/**
* Filter to modify the query arguments for anonymizing refunded orders.
*
* @since 9.8.0
*
* @param string $date_created The date before which orders should be anonymized.
* @param int $limit The maximum number of orders to process in each batch.
* @param string $status The status of the orders to be anonymized.
* @param string $type The type of orders to be anonymized.
*/
apply_filters(
'woocommerce_anonymize_refunded_orders_query_args',
array(
'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ),
'limit' => $limit, // Batches of 20.
'status' => OrderInternalStatus::REFUNDED,
'type' => 'shop_order',
'anonymized' => false,
)
)
);
}
/**
* For a given query trash all matches.
*
* @since 3.4.0
* @param array $query Query array to pass to wc_get_orders().
* @return int Count of orders that were trashed.
*/
protected static function trash_orders_query( $query ) {
$orders = wc_get_orders( $query );
$count = 0;
if ( $orders ) {
foreach ( $orders as $order ) {
$order->delete( false );
++$count;
}
}
return $count;
}
/**
* Anonymize old completed orders.
*
* @since 3.4.0
* @param int $limit Limit orders to process per batch.
* @return int Number of orders processed.
*/
public static function anonymize_completed_orders( $limit = 20 ) {
$option = wc_parse_relative_date_option( get_option( 'woocommerce_anonymize_completed_orders' ) );
if ( empty( $option['number'] ) ) {
return 0;
}
return self::anonymize_orders_query(
apply_filters(
'woocommerce_anonymize_completed_orders_query_args',
array(
'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ),
'limit' => $limit, // Batches of 20.
'status' => OrderInternalStatus::COMPLETED,
'anonymized' => false,
'type' => 'shop_order',
)
)
);
}
/**
* For a given query, anonymize all matches.
*
* @since 3.4.0
* @param array $query Query array to pass to wc_get_orders().
* @return int Count of orders that were anonymized.
*/
protected static function anonymize_orders_query( $query ) {
$orders = wc_get_orders( $query );
$count = 0;
if ( $orders ) {
foreach ( $orders as $order ) {
WC_Privacy_Erasers::remove_order_personal_data( $order );
++$count;
}
}
return $count;
}
/**
* Delete inactive accounts.
*
* @since 3.4.0
* @param int $limit Limit users to process per batch.
* @return int Number of users processed.
*/
public static function delete_inactive_accounts( $limit = 20 ) {
$option = wc_parse_relative_date_option( get_option( 'woocommerce_delete_inactive_accounts' ) );
if ( empty( $option['number'] ) ) {
return 0;
}
return self::delete_inactive_accounts_query( strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), $limit );
}
/**
* Delete inactive accounts.
*
* @since 3.4.0
* @param int $timestamp Timestamp to delete customers before.
* @param int $limit Limit number of users to delete per run.
* @return int Count of customers that were deleted.
*/
protected static function delete_inactive_accounts_query( $timestamp, $limit = 20 ) {
$count = 0;
$user_query = new WP_User_Query(
array(
'fields' => 'ID',
'number' => $limit,
'role__in' => apply_filters(
'woocommerce_delete_inactive_account_roles',
array(
'Customer',
'Subscriber',
)
),
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'relation' => 'AND',
array(
'key' => 'wc_last_active',
'value' => (string) $timestamp,
'compare' => '<',
'type' => 'NUMERIC',
),
array(
'key' => 'wc_last_active',
'value' => '0',
'compare' => '>',
'type' => 'NUMERIC',
),
),
)
);
$user_ids = $user_query->get_results();
if ( $user_ids ) {
if ( ! function_exists( 'wp_delete_user' ) ) {
require_once ABSPATH . 'wp-admin/includes/user.php';
}
foreach ( $user_ids as $user_id ) {
wp_delete_user( $user_id, 0 );
wc_get_logger()->info(
sprintf(
/* translators: %d user ID. */
__( "User #%d was deleted by WooCommerce in accordance with the site's personal data retention settings. Any content belonging to that user has been retained but unassigned.", 'woocommerce' ),
$user_id
)
);
++$count;
}
}
return $count;
}
}
new WC_Privacy();
/**
* Class WC_Product_Variation_Data_Store_CPT file.
*
* @package WooCommerce\DataStores
*/
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Enums\CatalogVisibility;
use Automattic\WooCommerce\Enums\ProductStockStatus;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Variation Product Data Store: Stored in CPT.
*
* @version 3.0.0
*/
class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT implements WC_Object_Data_Store_Interface {
/**
* Callback to remove unwanted meta data.
*
* @param object $meta Meta object.
* @return bool false if excluded.
*/
protected function exclude_internal_meta_keys( $meta ) {
$internal_meta_keys = $this->internal_meta_keys;
$internal_meta_keys[] = '_cogs_value_is_additive';
return ! in_array( $meta->meta_key, $internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'attribute_' ) && 0 !== stripos( $meta->meta_key, 'wp_' );
}
/*
|--------------------------------------------------------------------------
| CRUD Methods
|--------------------------------------------------------------------------
*/
/**
* Reads a product from the database and sets its data to the class.
*
* @since 3.0.0
* @param WC_Product_Variation $product Product object.
* @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status (via read_product_data), or when passing an invalid ID.
*/
public function read( &$product ) {
$product->set_defaults();
if ( ! $product->get_id() ) {
return;
}
$post_object = get_post( $product->get_id() );
if ( ! $post_object ) {
return;
}
if ( 'product_variation' !== $post_object->post_type ) {
throw new WC_Data_Exception( 'variation_invalid_id', __( 'Invalid product type: passed ID does not correspond to a product variation.', 'woocommerce' ) );
}
$product->set_props(
array(
'name' => $post_object->post_title,
'slug' => $post_object->post_name,
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
'status' => $post_object->post_status,
'menu_order' => $post_object->menu_order,
'reviews_allowed' => 'open' === $post_object->comment_status,
'parent_id' => $post_object->post_parent,
'attribute_summary' => $post_object->post_excerpt,
)
);
// The post parent is not a valid variable product so we should prevent this.
if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) {
$product->set_parent_id( 0 );
}
$this->read_downloads( $product );
$this->read_product_data( $product );
$this->read_extra_data( $product );
$product->set_attributes( wc_get_product_variation_attributes( $product->get_id() ) );
$updates = array();
/**
* If a variation title is not in sync with the parent e.g. saved prior to 3.0, or if the parent title has changed, detect here and update.
*/
$new_title = $this->generate_product_title( $product );
if ( $post_object->post_title !== $new_title ) {
$product->set_name( $new_title );
$updates = array_merge( $updates, array( 'post_title' => $new_title ) );
}
if ( ! empty( $updates ) ) {
$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $updates, array( 'ID' => $product->get_id() ) );
clean_post_cache( $product->get_id() );
}
// Set object_read true once all data is read.
$product->set_object_read( true );
}
/**
* Create a new product.
*
* @since 3.0.0
* @param WC_Product_Variation $product Product object.
*/
public function create( &$product ) {
if ( ! $product->get_date_created() ) {
$product->set_date_created( time() );
}
$new_title = $this->generate_product_title( $product );
if ( $product->get_name( 'edit' ) !== $new_title ) {
$product->set_name( $new_title );
}
$attribute_summary = $this->generate_attribute_summary( $product );
$product->set_attribute_summary( $attribute_summary );
// The post parent is not a valid variable product so we should prevent this.
if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) {
$product->set_parent_id( 0 );
}
$id = wp_insert_post(
apply_filters(
'woocommerce_new_product_variation_data',
array(
'post_type' => 'product_variation',
'post_status' => $product->get_status() ? $product->get_status() : ProductStatus::PUBLISH,
'post_author' => get_current_user_id(),
'post_title' => $product->get_name( 'edit' ),
'post_excerpt' => $product->get_attribute_summary( 'edit' ),
'post_content' => '',
'post_parent' => $product->get_parent_id(),
'comment_status' => 'closed',
'ping_status' => 'closed',
'menu_order' => $product->get_menu_order(),
'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
'post_name' => $product->get_slug( 'edit' ),
)
),
true
);
if ( $id && ! is_wp_error( $id ) ) {
$product->set_id( $id );
$this->update_post_meta( $product, true );
$this->update_terms( $product, true );
$this->update_visibility( $product, true );
$this->update_attributes( $product, true );
$this->handle_updated_props( $product );
$product->save_meta_data();
$product->apply_changes();
$this->update_version_and_type( $product );
$this->update_guid( $product );
$this->clear_caches( $product );
do_action( 'woocommerce_new_product_variation', $id, $product );
}
}
/**
* Updates an existing product.
*
* @since 3.0.0
* @param WC_Product_Variation $product Product object.
*/
public function update( &$product ) {
$product->save_meta_data();
if ( ! $product->get_date_created() ) {
$product->set_date_created( time() );
}
$new_title = $this->generate_product_title( $product );
if ( $product->get_name( 'edit' ) !== $new_title ) {
$product->set_name( $new_title );
}
// The post parent is not a valid variable product so we should prevent this.
if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) {
$product->set_parent_id( 0 );
}
$changes = $product->get_changes();
// Always recompute and sync the attribute summary as a safety net.
// This ensures it's up-to-date not just for direct attribute changes (e.g., via $changes['attributes']),
// but also for indirect desyncs, like when a global term (e.g., 'Blue' -> 'Blue2') is updated elsewhere.
// We ideally handle those at the source (e.g., global term update hooks), but this provides a fallback.
$new_attribute_summary = $this->generate_attribute_summary( $product );
// Compare the fresh attribute summary with the stored summary and update if out of sync.
if ( $new_attribute_summary !== $product->get_attribute_summary() ) {
$product->set_attribute_summary( $new_attribute_summary );
// If attributes weren't explicitly changed in this update, flag it to ensure the product is saved.
// This acts as a "just-in-case" trigger for indirect desyncs.
if ( ! isset( $changes['attributes'] ) ) {
$changes['attributes'] = true;
}
}
// Only update the post when the post data changes.
if ( array_intersect( array( 'name', 'parent_id', 'status', 'menu_order', 'date_created', 'date_modified', 'attributes' ), array_keys( $changes ) ) ) {
$post_data = array(
'post_title' => $product->get_name( 'edit' ),
'post_excerpt' => $product->get_attribute_summary( 'edit' ),
'post_parent' => $product->get_parent_id( 'edit' ),
'comment_status' => 'closed',
'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : ProductStatus::PUBLISH,
'menu_order' => $product->get_menu_order( 'edit' ),
'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ),
'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ),
'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ),
'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ),
'post_type' => 'product_variation',
'post_name' => $product->get_slug( 'edit' ),
);
/**
* When updating this object, to prevent infinite loops, use $wpdb
* to update data, since wp_update_post spawns more calls to the
* save_post action.
*
* This ensures hooks are fired by either WP itself (admin screen save),
* or an update purely from CRUD.
*/
if ( doing_action( 'save_post' ) ) {
$GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) );
clean_post_cache( $product->get_id() );
} else {
wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) );
}
$product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook.
} else { // Only update post modified time to record this save event.
$GLOBALS['wpdb']->update(
$GLOBALS['wpdb']->posts,
array(
'post_modified' => current_time( 'mysql' ),
'post_modified_gmt' => current_time( 'mysql', 1 ),
),
array(
'ID' => $product->get_id(),
)
);
clean_post_cache( $product->get_id() );
}
$this->update_post_meta( $product );
$this->update_terms( $product );
$this->update_visibility( $product, true );
$this->update_attributes( $product );
$this->handle_updated_props( $product );
$product->apply_changes();
$this->update_version_and_type( $product );
$this->clear_caches( $product );
do_action( 'woocommerce_update_product_variation', $product->get_id(), $product );
}
/*
|--------------------------------------------------------------------------
| Additional Methods
|--------------------------------------------------------------------------
*/
/**
* Generates a title with attribute information for a variation.
* Products will get a title of the form "Name - Value, Value" or just "Name".
*
* @since 3.0.0
* @param WC_Product $product Product object.
* @return string
*/
protected function generate_product_title( $product ) {
$attributes = (array) $product->get_attributes();
// Do not include attributes if the product has 3+ attributes.
$should_include_attributes = count( $attributes ) < 3;
// Do not include attributes if an attribute name has 2+ words and the
// product has multiple attributes.
if ( $should_include_attributes && 1 < count( $attributes ) ) {
foreach ( $attributes as $name => $value ) {
if ( false !== strpos( $name, '-' ) ) {
$should_include_attributes = false;
break;
}
}
}
$should_include_attributes = apply_filters( 'woocommerce_product_variation_title_include_attributes', $should_include_attributes, $product );
$separator = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $product );
$title_base = get_post_field( 'post_title', $product->get_parent_id() );
$title_suffix = $should_include_attributes ? wc_get_formatted_variation( $product, true, false ) : '';
return apply_filters( 'woocommerce_product_variation_title', $title_suffix ? $title_base . $separator . $title_suffix : $title_base, $product, $title_base, $title_suffix );
}
/**
* Generates attribute summary for the variation.
*
* Attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes.
*
* @since 3.6.0
* @param WC_Product_Variation $product Product variation to generate the attribute summary for.
*
* @return string
*/
protected function generate_attribute_summary( $product ) {
return wc_get_formatted_variation( $product, true, true );
}
/**
* Get attribute summary for a product.
*
* @param WC_Product $product The product object.
* @return string The generated attribute summary.
*/
public function get_attribute_summary( $product ) {
return $this->generate_attribute_summary( $product );
}
/**
* Make sure we store the product version (to track data changes).
*
* @param WC_Product $product Product object.
* @since 3.0.0
*/
protected function update_version_and_type( &$product ) {
wp_set_object_terms( $product->get_id(), '', 'product_type' );
update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) );
}
/**
* Read post data.
*
* @since 3.0.0
* @param WC_Product_Variation $product Product object.
* @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status.
*/
protected function read_product_data( &$product ) {
$id = $product->get_id();
$post_meta_values = get_post_meta( $id );
$meta_key_to_props = array(
'_variation_description' => 'description',
'_regular_price' => 'regular_price',
'_sale_price' => 'sale_price',
'_sale_price_dates_from' => 'date_on_sale_from',
'_sale_price_dates_to' => 'date_on_sale_to',
'_manage_stock' => 'manage_stock',
'_stock_status' => 'stock_status',
'_virtual' => 'virtual',
'_product_image_gallery' => 'gallery_image_ids',
'_download_limit' => 'download_limit',
'_download_expiry' => 'download_expiry',
'_downloadable' => 'downloadable',
'_sku' => 'sku',
'_global_unique_id' => 'global_unique_id',
'_stock' => 'stock_quantity',
'_weight' => 'weight',
'_length' => 'length',
'_width' => 'width',
'_height' => 'height',
'_low_stock_amount' => 'low_stock_amount',
'_backorders' => 'backorders',
'_cogs_total_value' => 'cogs_total_value',
'_cogs_value_is_additive' => 'cogs_value_is_additive',
'_tax_class' => 'tax_class',
);
$variation_data = array();
foreach ( $meta_key_to_props as $meta_key => $prop ) {
$meta_value = $post_meta_values[ $meta_key ][0] ?? '';
$variation_data[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values.
}
$variation_data['gallery_image_ids'] = array_filter( explode( ',', $variation_data['gallery_image_ids'] ?? '' ) );
$variation_data['shipping_class_id'] = current( $this->get_term_ids( $id, 'product_shipping_class' ) );
$variation_data['image_id'] = get_post_thumbnail_id( $id );
$variation_data['tax_class'] = ! metadata_exists( 'post', $id, '_tax_class' ) ? 'parent' : $variation_data['tax_class'];
$product->set_props( $variation_data );
if ( $this->cogs_feature_is_enabled() ) {
$cogs_value = $variation_data['cogs_total_value'] ?? '';
$cogs_value = '' === $cogs_value ? null : (float) $cogs_value;
$cogs_value_is_additive = 'yes' === ( $variation_data['cogs_value_is_additive'] ?? '' );
$product->set_props(
array(
'cogs_value' => $cogs_value,
'cogs_value_is_additive' => $cogs_value_is_additive,
)
);
}
if ( $product->is_on_sale( 'edit' ) ) {
$product->set_price( $product->get_sale_price( 'edit' ) );
} else {
$product->set_price( $product->get_regular_price( 'edit' ) );
}
$parent_id = $product->get_parent_id();
$parent_object = get_post( $parent_id );
$terms = get_the_terms( $parent_id, 'product_visibility' );
$term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array();
$exclude_search = in_array( 'exclude-from-search', $term_names, true );
$exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true );
if ( $exclude_search && $exclude_catalog ) {
$catalog_visibility = CatalogVisibility::HIDDEN;
} elseif ( $exclude_search ) {
$catalog_visibility = CatalogVisibility::CATALOG;
} elseif ( $exclude_catalog ) {
$catalog_visibility = CatalogVisibility::SEARCH;
} else {
$catalog_visibility = CatalogVisibility::VISIBLE;
}
$parent_post_meta_values = get_post_meta( $parent_id );
$parent_meta_key_to_props = array(
'_sku' => 'sku',
'_global_unique_id' => 'global_unique_id',
'_manage_stock' => 'manage_stock',
'_backorders' => 'backorders',
'_stock' => 'stock_quantity',
'_weight' => 'weight',
'_length' => 'length',
'_width' => 'width',
'_height' => 'height',
'_tax_class' => 'tax_class',
'_purchase_note' => 'purchase_note',
'_sold_individually' => 'sold_individually',
'_tax_status' => 'tax_status',
'_crosssell_ids' => '_crosssell_ids',
);
$parent_data = array();
foreach ( $parent_meta_key_to_props as $meta_key => $prop ) {
$meta_value = $parent_post_meta_values[ $meta_key ][0] ?? '';
$parent_data[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values.
}
$parent_data['title'] = $parent_object ? $parent_object->post_title : '';
$parent_data['status'] = $parent_object ? $parent_object->post_status : '';
$parent_data['shipping_class_id'] = absint( current( $this->get_term_ids( $parent_id, 'product_shipping_class' ) ) );
$parent_data['catalog_visibility'] = $catalog_visibility;
$parent_data['stock_quantity'] = wc_stock_amount( $parent_data['stock_quantity'] );
$parent_data['image_id'] = get_post_thumbnail_id( $parent_id );
$product->set_parent_data( $parent_data );
// Pull data from the parent when there is no user-facing way to set props.
$product->set_sold_individually( $parent_data['sold_individually'] );
$product->set_tax_status( $parent_data['tax_status'] );
$product->set_cross_sell_ids( $parent_data['_crosssell_ids'] );
if ( $this->cogs_feature_is_enabled() ) {
$this->load_cogs_data( $product );
}
}
/**
* Load the Cost of Goods Sold related data for a given product.
*
* @param WC_Product $product The product to apply the loaded data to.
*/
protected function load_cogs_data( $product ) {
parent::load_cogs_data( $product );
$cogs_value_is_additive = 'yes' === get_post_meta( $product->get_id(), '_cogs_value_is_additive', true );
/**
* Filter to customize the "Cost of Goods Sold value is additive" flag that gets loaded for a given variable product.
*
* @since 9.7.0
*
* @param bool $cogs_value_is_additive The flag as read from the database.
* @param WC_Product $product The product for which the flag is being loaded.
*/
$cogs_value_is_additive = apply_filters( 'woocommerce_load_product_cogs_is_additive_flag', $cogs_value_is_additive, $product );
$product->set_props(
array(
'cogs_value_is_additive' => $cogs_value_is_additive,
)
);
}
/**
* For all stored terms in all taxonomies, save them to the DB.
*
* @since 3.0.0
* @param WC_Product $product Product object.
* @param bool $force Force update. Used during create.
*/
protected function update_terms( &$product, $force = false ) {
$changes = $product->get_changes();
if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) {
wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false );
}
}
/**
* Update visibility terms based on props.
*
* @since 3.0.0
*
* @param WC_Product $product Product object.
* @param bool $force Force update. Used during create.
*/
protected function update_visibility( &$product, $force = false ) {
$changes = $product->get_changes();
if ( $force || array_intersect( array( 'stock_status' ), array_keys( $changes ) ) ) {
$terms = array();
if ( ProductStockStatus::OUT_OF_STOCK === $product->get_stock_status() ) {
$terms[] = ProductStockStatus::OUT_OF_STOCK;
}
wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false );
}
}
/**
* Update attribute meta values.
*
* @since 3.0.0
* @param WC_Product $product Product object.
* @param bool $force Force update. Used during create.
*/
protected function update_attributes( &$product, $force = false ) {
$changes = $product->get_changes();
if ( $force || array_key_exists( 'attributes', $changes ) ) {
global $wpdb;
$product_id = $product->get_id();
$attributes = $product->get_attributes();
$updated_attribute_keys = array();
foreach ( $attributes as $key => $value ) {
update_post_meta( $product_id, 'attribute_' . $key, wp_slash( $value ) );
$updated_attribute_keys[] = 'attribute_' . $key;
}
// Remove old taxonomies attributes so data is kept up to date - first get attribute key names.
$delete_attribute_keys = $wpdb->get_col(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration
"SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d",
$wpdb->esc_like( 'attribute_' ) . '%',
$product_id
)
);
foreach ( $delete_attribute_keys as $key ) {
delete_post_meta( $product_id, $key );
}
}
}
/**
* Helper method that updates all the post meta for a product based on its settings in the WC_Product class.
*
* @since 3.0.0
* @param WC_Product $product Product object.
* @param bool $force Force update. Used during create.
*/
public function update_post_meta( &$product, $force = false ) {
$meta_key_to_props = array(
'_variation_description' => 'description',
);
$props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props );
foreach ( $props_to_update as $meta_key => $prop ) {
$value = $product->{"get_$prop"}( 'edit' );
$updated = update_post_meta( $product->get_id(), $meta_key, $value );
if ( $updated ) {
$this->updated_props[] = $prop;
}
}
if ( $this->cogs_feature_is_enabled() ) {
$cogs_value_is_additive = $product->get_cogs_value_is_additive();
/**
* Filter to customize the "Cost of Goods Sold value is additive" flag that gets saved for a given variable product,
* or to suppress the saving of the flag (so that custom storage can be used) if null is returned.
* Note that returning null will suppress any database access (for either saving the flag or deleting it).
*
* @since 9.7.0
*
* @param bool|null $cogs_value_is_additive The flag to be written to the database. If null is returned nothing will be written or deleted.
* @param WC_Product $product The product for which the flag is being saved.
*/
$cogs_value_is_additive = apply_filters( 'woocommerce_save_product_cogs_is_additive_flag', $cogs_value_is_additive, $product );
if ( ! is_null( $cogs_value_is_additive ) ) {
$updated = $this->update_or_delete_post_meta( $product, '_cogs_value_is_additive', $cogs_value_is_additive ? 'yes' : '' );
if ( $updated ) {
$this->updated_props[] = 'cogs_value_is_additive';
}
}
}
parent::update_post_meta( $product, $force );
}
/**
* Update product variation guid.
*
* @param WC_Product_Variation $product Product variation object.
*
* @since 3.6.0
*/
protected function update_guid( $product ) {
global $wpdb;
$guid = home_url(
add_query_arg(
array(
'post_type' => 'product_variation',
'p' => $product->get_id(),
),
''
)
);
$wpdb->update( $wpdb->posts, array( 'guid' => $guid ), array( 'ID' => $product->get_id() ) );
}
}
/**
* Class WC_Order_Item_Product_Data_Store file.
*
* @package WooCommerce\DataStores
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WC Order Item Product Data Store
*
* @version 3.0.0
*/
class WC_Order_Item_Product_Data_Store extends Abstract_WC_Order_Item_Type_Data_Store implements WC_Object_Data_Store_Interface, WC_Order_Item_Product_Data_Store_Interface, WC_Order_Item_Type_Data_Store_Interface {
/**
* Data stored in meta keys.
*
* @since 3.0.0
* @var array
*/
protected $internal_meta_keys = array( '_product_id', '_variation_id', '_qty', '_tax_class', '_line_subtotal', '_line_subtotal_tax', '_line_total', '_line_tax', '_line_tax_data' );
/**
* Read/populate data properties specific to this order item.
*
* @since 3.0.0
* @param WC_Order_Item_Product $item Product order item object.
*/
public function read( &$item ) {
parent::read( $item );
$id = $item->get_id();
$item->set_props(
array(
'product_id' => get_metadata( 'order_item', $id, '_product_id', true ),
'variation_id' => get_metadata( 'order_item', $id, '_variation_id', true ),
'quantity' => get_metadata( 'order_item', $id, '_qty', true ),
'tax_class' => get_metadata( 'order_item', $id, '_tax_class', true ),
'subtotal' => get_metadata( 'order_item', $id, '_line_subtotal', true ),
'total' => get_metadata( 'order_item', $id, '_line_total', true ),
'taxes' => get_metadata( 'order_item', $id, '_line_tax_data', true ),
)
);
$item->set_object_read( true );
}
/**
* Saves an item's data to the database / item meta.
* Ran after both create and update, so $id will be set.
*
* @since 3.0.0
* @param WC_Order_Item_Product $item Product order item object.
*/
public function save_item_data( &$item ) {
$id = $item->get_id();
$changes = $item->get_changes();
$meta_key_to_props = array(
'_product_id' => 'product_id',
'_variation_id' => 'variation_id',
'_qty' => 'quantity',
'_tax_class' => 'tax_class',
'_line_subtotal' => 'subtotal',
'_line_subtotal_tax' => 'subtotal_tax',
'_line_total' => 'total',
'_line_tax' => 'total_tax',
'_line_tax_data' => 'taxes',
);
$props_to_update = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' );
foreach ( $props_to_update as $meta_key => $prop ) {
update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) );
}
}
/**
* Get a list of download IDs for a specific item from an order.
*
* @since 3.0.0
* @param WC_Order_Item_Product $item Product order item object.
* @param WC_Order $order Order object.
* @return array
*/
public function get_download_ids( $item, $order ) {
global $wpdb;
return $wpdb->get_col(
$wpdb->prepare(
"SELECT download_id FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE user_email = %s AND order_key = %s AND product_id = %d ORDER BY permission_id",
$order->get_billing_email(),
$order->get_order_key(),
$item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id()
)
);
}
}