<?php
/**
* WooCommerce sync module.
*
* @package automattic/jetpack-sync
*/
namespace Automattic\Jetpack\Sync\Modules;
use WC_Order;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class to handle sync for WooCommerce.
*/
class WooCommerce extends Module {
/**
* Whitelist for order item meta we are interested to sync.
*
* @access private
*
* @var array
*/
public static $order_item_meta_whitelist = array(
// See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-product-store.php#L20 .
'_product_id',
'_variation_id',
'_qty',
// Tax ones also included in below class
// See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-fee-data-store.php#L20 .
'_tax_class',
'_tax_status',
'_line_subtotal',
'_line_subtotal_tax',
'_line_total',
'_line_tax',
'_line_tax_data',
// See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-shipping-data-store.php#L20 .
'method_id',
'cost',
'total_tax',
'taxes',
// See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-tax-data-store.php#L20 .
'rate_id',
'label',
'compound',
'tax_amount',
'shipping_tax_amount',
// See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-coupon-data-store.php .
'discount_amount',
'discount_amount_tax',
);
/**
* Mapping between WooCommerce customer detail user meta keys and customer prop names.
*
* @access private
*
* @var array
*/
private static $customer_detail_meta_key_to_prop = array(
'paying_customer' => 'is_paying_customer',
'billing_first_name' => 'billing_first_name',
'billing_last_name' => 'billing_last_name',
'billing_company' => 'billing_company',
'billing_address_1' => 'billing_address_1',
'billing_address_2' => 'billing_address_2',
'billing_city' => 'billing_city',
'billing_state' => 'billing_state',
'billing_postcode' => 'billing_postcode',
'billing_country' => 'billing_country',
'billing_email' => 'billing_email',
'billing_phone' => 'billing_phone',
'shipping_first_name' => 'shipping_first_name',
'shipping_last_name' => 'shipping_last_name',
'shipping_company' => 'shipping_company',
'shipping_address_1' => 'shipping_address_1',
'shipping_address_2' => 'shipping_address_2',
'shipping_city' => 'shipping_city',
'shipping_state' => 'shipping_state',
'shipping_postcode' => 'shipping_postcode',
'shipping_country' => 'shipping_country',
'shipping_phone' => 'shipping_phone',
);
/**
* Name of the order item database table.
*
* @access private
*
* @var string
*/
private $order_item_table_name;
/**
* Customer detail meta changes to sync at the end of the request.
*
* @var array
*/
private $customer_meta_updates = array();
/**
* User IDs deleted during the current request.
*
* @var array
*/
private $deleted_user_ids = array();
/**
* The table name.
*
* @access public
*
* @return string
* @deprecated since 3.11.0 Use table() instead.
*/
public function table_name() {
_deprecated_function( __METHOD__, '3.11.0', 'Automattic\\Jetpack\\Sync\\WooCommerce->table' );
return $this->order_item_table_name;
}
/**
* The table in the database with the prefix.
*
* @access public
*
* @return string|bool
*/
public function table() {
global $wpdb;
return $wpdb->prefix . 'woocommerce_order_items';
}
/**
* The id field in the database.
*
* @access public
*
* @return string
*/
public function id_field() {
return 'order_item_id';
}
/**
* The full sync action name for this module.
*
* @access public
*
* @return string
*/
public function full_sync_action_name() {
return 'jetpack_full_sync_woocommerce_order_items';
}
/**
* Constructor.
*
* @global $wpdb
*
* @todo Should we refactor this to use $this->set_defaults() instead?
*/
public function __construct() {
global $wpdb;
$this->order_item_table_name = $wpdb->prefix . 'woocommerce_order_items';
// Options, constants and post meta whitelists.
add_filter( 'jetpack_sync_options_whitelist', array( $this, 'add_woocommerce_options_whitelist' ), 10 );
add_filter( 'jetpack_sync_constants_whitelist', array( $this, 'add_woocommerce_constants_whitelist' ), 10 );
add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'add_woocommerce_post_meta_whitelist' ), 10 );
add_filter( 'jetpack_sync_comment_meta_whitelist', array( $this, 'add_woocommerce_comment_meta_whitelist' ), 10 );
add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_order_item', array( $this, 'filter_order_item' ) );
add_filter( 'jetpack_sync_before_enqueue_jetpack_updated_woo_customer_meta', array( $this, 'filter_customer_updated_meta' ) );
add_filter( 'jetpack_sync_whitelisted_comment_types', array( $this, 'add_review_comment_types' ) );
// Blacklist Action Scheduler comment types.
add_filter( 'jetpack_sync_prevent_sending_comment_data', array( $this, 'filter_action_scheduler_comments' ), 10, 2 );
// Preprocess action to be sent by Jetpack sync.
add_action( 'woocommerce_remove_order_items', array( $this, 'action_woocommerce_remove_order_items' ), 10, 2 );
}
/**
* Sync module name.
*
* @access public
*
* @return string
*/
public function name() {
return 'woocommerce';
}
/**
* Initialize WooCommerce action listeners.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_listeners( $callable ) {
// Attributes.
add_action( 'woocommerce_attribute_added', $callable, 10, 2 );
add_action( 'woocommerce_attribute_updated', $callable, 10, 3 );
add_action( 'woocommerce_attribute_deleted', $callable, 10, 3 );
// Orders.
add_action( 'woocommerce_new_order', $callable, 10, 1 );
add_action( 'woocommerce_order_status_changed', $callable, 10, 3 );
add_action( 'woocommerce_payment_complete', $callable, 10, 1 );
// Order items.
add_action( 'woocommerce_new_order_item', $callable, 10, 4 );
add_action( 'woocommerce_delete_order_item', $callable, 10, 1 );
add_action( 'woocommerce_remove_order_item_ids', $callable, 10, 1 );
$this->init_listeners_for_meta_type( 'order_item', $callable );
$this->init_meta_whitelist_handler( 'order_item', array( $this, 'filter_meta' ) );
// Payment tokens.
add_action( 'woocommerce_new_payment_token', $callable, 10, 1 );
add_action( 'woocommerce_payment_token_deleted', $callable, 10, 2 );
add_action( 'woocommerce_payment_token_updated', $callable, 10, 1 );
$this->init_listeners_for_meta_type( 'payment_token', $callable );
// Product downloads.
add_action( 'woocommerce_downloadable_product_download_log_insert', $callable, 10, 1 );
add_action( 'woocommerce_grant_product_download_access', $callable, 10, 1 );
// Tax rates.
// These are ignored on WP.com: tax items are derived from order data via wc_order_tax_lookup, which isn’t present there.
add_action( 'woocommerce_tax_rate_added', $callable, 10, 2 );
add_action( 'woocommerce_tax_rate_updated', $callable, 10, 2 );
add_action( 'woocommerce_tax_rate_deleted', $callable, 10, 1 );
// Webhooks.
add_action( 'woocommerce_new_webhook', $callable, 10, 1 );
add_action( 'woocommerce_webhook_deleted', $callable, 10, 2 );
add_action( 'woocommerce_webhook_updated', $callable, 10, 1 );
// Customers.
add_action( 'added_user_meta', array( $this, 'maybe_sync_customer_meta_update' ), 10, 4 );
add_action( 'updated_user_meta', array( $this, 'maybe_sync_customer_meta_update' ), 10, 4 );
add_action( 'deleted_user_meta', array( $this, 'maybe_sync_customer_meta_update' ), 10, 4 );
add_action( 'delete_user', array( $this, 'action_delete_user' ), 10, 1 );
add_action( 'wpmu_delete_user', array( $this, 'action_delete_user' ), 10, 1 );
add_action( 'shutdown', array( $this, 'action_customer_meta_updates' ) );
add_action( 'jetpack_updated_woo_customer_meta', $callable, 10, 2 );
}
/**
* Initialize WooCommerce action listeners for full sync.
*
* @access public
*
* @param callable $callable Action handler callable.
*/
public function init_full_sync_listeners( $callable ) {
add_action( 'jetpack_full_sync_woocommerce_order_items', $callable ); // Also sends post meta.
}
/**
* Retrieve the actions that will be sent for this module during a full sync.
*
* @access public
*
* @return array Full sync actions of this module.
*/
public function get_full_sync_actions() {
return array( 'jetpack_full_sync_woocommerce_order_items' );
}
/**
* Initialize the module in the sender.
*
* @access public
*/
public function init_before_send() {
// Full sync.
add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_order_items', array( $this, 'build_full_sync_action_array' ) );
}
/**
* Expand the order items properly.
*
* @access public
*
* @param array $args The hook arguments.
* @return array $args The hook arguments.
*/
public function filter_order_item( $args ) {
// Make sure we always have all the data - prior to WooCommerce 3.0 we only have the user supplied data in the second argument and not the full details.
$args[1] = $this->build_order_item( $args[0] );
return $args;
}
/**
* Validate the minimal customer meta update payload before enqueueing.
*
* @param array $args Hook arguments.
* @return array|false Minimal user object and changed prop names, or false when invalid.
*/
public function filter_customer_updated_meta( $args ) {
if (
! is_array( $args )
|| ! isset( $args[0] )
|| ! isset( $args[1] )
|| ! is_object( $args[0] )
|| ! isset( $args[0]->data )
|| ! is_object( $args[0]->data )
|| ! isset( $args[0]->data->ID )
|| ! is_numeric( $args[0]->data->ID )
|| ! is_array( $args[1] )
) {
return false;
}
$customer_id = (int) $args[0]->data->ID;
if ( $customer_id <= 0 ) {
return false;
}
$updated_props = $this->get_customer_detail_props( $args[1] );
if ( empty( $updated_props ) ) {
return false;
}
return array( $this->build_minimal_customer_user_object( $customer_id ), $updated_props );
}
/**
* Track updated WooCommerce customer meta props for syncing.
*
* @param int|array $meta_id ID of the meta object, or IDs for deleted meta.
* @param int $user_id User ID.
* @param string $meta_key Meta key.
* @param mixed $value Meta value.
*/
public function maybe_sync_customer_meta_update( $meta_id, $user_id, $meta_key, $value ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$customer_id = (int) $user_id;
if ( $customer_id <= 0 ) {
return;
}
if ( 'deleted_user_meta' === current_action() && isset( $this->deleted_user_ids[ $customer_id ] ) ) {
return;
}
if ( ! is_string( $meta_key ) && ! is_numeric( $meta_key ) ) {
return;
}
$meta_key = sanitize_key( (string) $meta_key );
if ( ! isset( self::$customer_detail_meta_key_to_prop[ $meta_key ] ) ) {
return;
}
$updated_prop = self::$customer_detail_meta_key_to_prop[ $meta_key ];
if ( ! isset( $this->customer_meta_updates[ $customer_id ] ) ) {
$this->customer_meta_updates[ $customer_id ] = array();
}
$this->customer_meta_updates[ $customer_id ][ $updated_prop ] = true;
}
/**
* Mark a deleted user so customer meta cleanup does not sync as profile changes.
*
* @param int $user_id User ID.
*/
public function action_delete_user( $user_id ) {
$customer_id = (int) $user_id;
if ( $customer_id <= 0 ) {
return;
}
$this->deleted_user_ids[ $customer_id ] = true;
unset( $this->customer_meta_updates[ $customer_id ] );
}
/**
* Send batched WooCommerce customer meta updates.
*/
public function action_customer_meta_updates() {
if ( empty( $this->customer_meta_updates ) ) {
return;
}
$customer_meta_updates = $this->customer_meta_updates;
$this->customer_meta_updates = array();
foreach ( $customer_meta_updates as $customer_id => $updated_props ) {
if ( isset( $this->deleted_user_ids[ (int) $customer_id ] ) ) {
continue;
}
/**
* Fires when WooCommerce customer details stored in user meta are updated.
*
* @param object $customer Minimal WP_User-shaped customer object.
* @param array $updated_props Updated customer detail prop names.
*/
do_action(
'jetpack_updated_woo_customer_meta',
$this->build_minimal_customer_user_object( (int) $customer_id ),
array_keys( $updated_props )
);
}
}
/**
* Retrieve whitelisted WooCommerce customer detail props.
*
* @param array $props Customer detail meta keys or prop names.
* @return array Customer detail prop names.
*/
private function get_customer_detail_props( $props ) {
$updated_props = array();
foreach ( $props as $prop ) {
if ( ! is_string( $prop ) && ! is_numeric( $prop ) ) {
continue;
}
$prop = sanitize_key( (string) $prop );
if ( isset( self::$customer_detail_meta_key_to_prop[ $prop ] ) ) {
$updated_props[] = self::$customer_detail_meta_key_to_prop[ $prop ];
continue;
}
if ( in_array( $prop, self::$customer_detail_meta_key_to_prop, true ) ) {
$updated_props[] = $prop;
}
}
return array_values( array_unique( $updated_props ) );
}
/**
* Build a minimal WP_User-shaped object for Activity Log.
*
* @param int $customer_id Customer user ID.
* @return object Minimal user object.
*/
private function build_minimal_customer_user_object( $customer_id ) {
$user_data = (object) array(
'ID' => $customer_id,
'display_name' => '',
'user_login' => '',
'user_email' => '',
);
$user = get_userdata( $customer_id );
if ( $user ) {
$user_data->display_name = (string) $user->display_name;
$user_data->user_login = (string) $user->user_login;
$user_data->user_email = (string) $user->user_email;
}
return (object) array(
'ID' => $customer_id,
'data' => $user_data,
);
}
/**
* Handler for filtering out non-whitelisted order item meta.
*
* @since 4.22.3
*
* @param array $args Hook arguments.
* @return array|false False if not whitelisted, the original hook args otherwise.
*/
public function filter_meta( $args ) {
if (
! empty( $args[2] ) && $this->is_whitelisted_order_item_meta( $args[2] )
) {
return $args;
}
return false;
}
/**
* Whether an order item meta key is whitelisted for sync.
*
* @access public
*
* @since 4.22.3
*
* @param string $meta_key Order item meta key.
* @return bool True if whitelisted.
*/
public function is_whitelisted_order_item_meta( $meta_key ) {
return is_string( $meta_key ) && in_array( $meta_key, self::$order_item_meta_whitelist, true );
}
/**
* Retrieve the order item ids to be removed and send them as one action
*
* @param WC_Order $order The order argument.
* @param string $type Order item type.
*/
public function action_woocommerce_remove_order_items( WC_Order $order, $type ) {
if ( $type ) {
$order_items = $order->get_items( $type );
} else {
$order_items = $order->get_items();
}
$order_item_ids = array_keys( $order_items );
if ( $order_item_ids ) {
do_action( 'woocommerce_remove_order_item_ids', $order_item_ids );
}
}
/**
* Expand order item IDs to order items and their meta.
*
* @access public
*
* @todo Refactor table name to use a $wpdb->prepare placeholder.
*
* @param array $args The hook arguments.
* @return array $args Expanded order items with meta.
* @deprecated since 4.7.0
*/
public function expand_order_item_ids( $args ) {
_deprecated_function( __METHOD__, '4.7.0' );
$order_item_ids = $args[0];
global $wpdb;
$order_item_ids_sql = implode( ', ', array_map( 'intval', $order_item_ids ) );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$order_items = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT * FROM $this->order_item_table_name WHERE order_item_id IN ( $order_item_ids_sql )"
);
return array(
$order_items,
$this->get_metadata( $order_item_ids, 'order_item', static::$order_item_meta_whitelist ),
);
}
/**
* Extract the full order item from the database by its ID.
*
* @access public
*
* @param int $order_item_id Order item ID.
* @return object Order item.
*/
public function build_order_item( $order_item_id ) {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct database access is intentional; caching is not required for this query.
return $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE order_item_id = %d', $this->order_item_table_name, $order_item_id ) );
}
/**
* Enqueue the WooCommerce actions for full sync.
*
* @access public
*
* @param array $config Full sync configuration for this sync module.
* @param int $max_items_to_enqueue Maximum number of items to enqueue.
* @param boolean $state True if full sync has finished enqueueing this module, false otherwise.
* @return array Number of actions enqueued, and next module state.
*/
public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
return $this->enqueue_all_ids_as_action( 'jetpack_full_sync_woocommerce_order_items', $this->order_item_table_name, 'order_item_id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
}
/**
* Retrieve an estimated number of actions that will be enqueued.
*
* @access public
*
* @todo Refactor the SQL query to use $wpdb->prepare().
*
* @param array $config Full sync configuration for this sync module.
* @return int Number of items yet to be enqueued.
*/
public function estimate_full_sync_actions( $config ) {
global $wpdb;
$query = "SELECT count(*) FROM $this->order_item_table_name WHERE " . $this->get_where_sql( $config );
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$count = (int) $wpdb->get_var( $query );
return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
}
/**
* Retrieve the WHERE SQL clause based on the module config.
*
* @access private
*
* @param array $config Full sync configuration for this sync module.
* @return string WHERE SQL clause.
*/
public function get_where_sql( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return '1=1';
}
/**
* Add WooCommerce options to the options whitelist.
*
* @param array $list Existing options whitelist.
* @return array Updated options whitelist.
*/
public function add_woocommerce_options_whitelist( $list ) {
return array_merge( $list, self::$wc_options_whitelist );
}
/**
* Add WooCommerce constants to the constants whitelist.
*
* @param array $list Existing constants whitelist.
* @return array Updated constants whitelist.
*/
public function add_woocommerce_constants_whitelist( $list ) {
return array_merge( $list, self::$wc_constants_whitelist );
}
/**
* Add WooCommerce post meta to the post meta whitelist.
*
* @param array $list Existing post meta whitelist.
* @return array Updated post meta whitelist.
*/
public function add_woocommerce_post_meta_whitelist( $list ) {
return array_merge( $list, self::$wc_post_meta_whitelist );
}
/**
* Add WooCommerce comment meta to the comment meta whitelist.
*
* @param array $list Existing comment meta whitelist.
* @return array Updated comment meta whitelist.
*/
public function add_woocommerce_comment_meta_whitelist( $list ) {
return array_merge( $list, self::$wc_comment_meta_whitelist );
}
/**
* Adds 'revew' to the list of comment types so Sync will listen for status changes on 'reviews'.
*
* @access public
*
* @param array $comment_types The list of comment types prior to this filter.
* return array The list of comment types with 'review' added.
*/
public function add_review_comment_types( $comment_types ) {
if ( is_array( $comment_types ) ) {
$comment_types[] = 'review';
}
return $comment_types;
}
/**
* Stop comments from the Action Scheduler from being synced.
* https://github.com/woocommerce/woocommerce/tree/e7762627c37ec1f7590e6cac4218ba0c6a20024d/includes/libraries/action-scheduler
*
* @since 1.6.3
* @since-jetpack 7.7.0
*
* @param boolean $can_sync Should we prevent comment data from bing synced to WordPress.com.
* @param mixed $comment WP_COMMENT object.
*
* @return bool
*/
public function filter_action_scheduler_comments( $can_sync, $comment ) {
if ( isset( $comment->comment_agent ) && 'ActionScheduler' === $comment->comment_agent ) {
return true;
}
return $can_sync;
}
/**
* Whitelist for options we are interested to sync.
*
* @access private
* @static
*
* @var array
*/
private static $wc_options_whitelist = array(
'woocommerce_currency',
'woocommerce_db_version',
'woocommerce_weight_unit',
'woocommerce_version',
'woocommerce_unforce_ssl_checkout',
'woocommerce_tax_total_display',
'woocommerce_tax_round_at_subtotal',
'woocommerce_tax_display_shop',
'woocommerce_tax_display_cart',
'woocommerce_prices_include_tax',
'woocommerce_price_thousand_sep',
'woocommerce_price_num_decimals',
'woocommerce_price_decimal_sep',
'woocommerce_notify_low_stock',
'woocommerce_notify_low_stock_amount',
'woocommerce_notify_no_stock',
'woocommerce_notify_no_stock_amount',
'woocommerce_manage_stock',
'woocommerce_force_ssl_checkout',
'woocommerce_hide_out_of_stock_items',
'woocommerce_file_download_method',
'woocommerce_enable_signup_and_login_from_checkout',
'woocommerce_enable_shipping_calc',
'woocommerce_enable_review_rating',
'woocommerce_enable_guest_checkout',
'woocommerce_enable_coupons',
'woocommerce_enable_checkout_login_reminder',
'woocommerce_enable_ajax_add_to_cart',
'woocommerce_dimension_unit',
'woocommerce_default_country',
'woocommerce_default_customer_address',
'woocommerce_currency_pos',
'woocommerce_api_enabled',
'woocommerce_allow_tracking',
'woocommerce_task_list_hidden',
'woocommerce_cod_settings',
'woocommerce_store_address',
'woocommerce_store_address_2',
'woocommerce_store_city',
'woocommerce_store_postcode',
'woocommerce_admin_install_timestamp',
'woocommerce_enable_signup_from_checkout_for_subscriptions', // This and the below options relate to the WooCommerce Accounts and Privacy settings page. Required for the Activity Log.
'woocommerce_enable_myaccount_registration',
'woocommerce_registration_generate_password',
'woocommerce_erasure_request_removes_order_data',
'woocommerce_erasure_request_removes_subscription_data',
'woocommerce_erasure_request_removes_download_data',
'woocommerce_allow_bulk_remove_personal_data',
'woocommerce_registration_privacy_policy_text',
'woocommerce_checkout_privacy_policy_text',
'woocommerce_delete_inactive_accounts',
'woocommerce_trash_pending_orders',
'woocommerce_trash_failed_orders',
'woocommerce_trash_cancelled_orders',
'woocommerce_anonymize_refunded_orders',
'woocommerce_anonymize_completed_orders',
'woocommerce_anonymize_ended_subscriptions',
'woocommerce_enable_delayed_account_creation',
'woocommerce_gateway_stripe_retention',
'wc_downloads_approved_directories_mode', // This and the below options relate to the WooCommerce Products settings page. Required for the Activity Log.
'woocommerce_attribute_lookup_direct_updates',
'woocommerce_attribute_lookup_enabled',
'woocommerce_attribute_lookup_optimized_updates',
'woocommerce_cart_redirect_after_add',
'woocommerce_downloads_add_hash_to_filename',
'woocommerce_downloads_count_partial',
'woocommerce_downloads_deliver_inline',
'woocommerce_downloads_grant_access_after_payment',
'woocommerce_downloads_redirect_fallback_allowed',
'woocommerce_downloads_require_login',
'woocommerce_enable_reviews',
'woocommerce_hold_stock_minutes',
'woocommerce_review_rating_required',
'woocommerce_review_rating_verification_label',
'woocommerce_review_rating_verification_required',
'woocommerce_shop_page_id',
'woocommerce_stock_email_recipient',
'woocommerce_stock_format',
'woocommerce_analytics_enabled', // This and the below options relate to the WooCommerce Advanced settings page. Required for the Activity Log.
'woocommerce_cart_page_id',
'woocommerce_checkout_order_received_endpoint',
'woocommerce_checkout_page_id',
'woocommerce_checkout_pay_endpoint',
'woocommerce_custom_orders_table_data_sync_enabled',
'woocommerce_custom_orders_table_enabled',
'woocommerce_feature_block_email_editor_enabled',
'woocommerce_feature_blueprint_enabled',
'woocommerce_feature_cost_of_goods_sold_enabled',
'woocommerce_feature_customer_review_request_enabled',
'woocommerce_feature_deferred_transactional_emails_enabled',
'woocommerce_feature_destroy-empty-sessions_enabled',
'woocommerce_feature_email_improvements_enabled',
'woocommerce_feature_mcp_integration_enabled',
'woocommerce_feature_order_attribution_enabled',
'woocommerce_feature_point_of_sale_enabled',
'woocommerce_feature_product_instance_caching_enabled',
'woocommerce_feature_rate_limit_checkout_enabled',
'woocommerce_feature_remote_logging_enabled',
'woocommerce_feature_rest_api_caching_enabled',
'woocommerce_feature_site_visibility_badge_enabled',
'woocommerce_hpos_datastore_caching_enabled',
'woocommerce_hpos_fts_index_enabled',
'woocommerce_logout_endpoint',
'woocommerce_myaccount_add_payment_method_endpoint',
'woocommerce_myaccount_delete_payment_method_endpoint',
'woocommerce_myaccount_downloads_endpoint',
'woocommerce_myaccount_edit_account_endpoint',
'woocommerce_myaccount_edit_address_endpoint',
'woocommerce_myaccount_lost_password_endpoint',
'woocommerce_myaccount_orders_endpoint',
'woocommerce_myaccount_page_id',
'woocommerce_myaccount_payment_methods_endpoint',
'woocommerce_myaccount_set_default_payment_method_endpoint',
'woocommerce_myaccount_subscription_payment_method_endpoint',
'woocommerce_myaccount_subscriptions_endpoint',
'woocommerce_myaccount_view_order_endpoint',
'woocommerce_myaccount_view_subscription_endpoint',
'woocommerce_show_marketplace_suggestions',
'woocommerce_terms_page_id',
'woocommerce_pickup_location_settings', // This and the below options relate to the WooCommerce Shipping settings page. Required for the Activity Log.
'pickup_location_pickup_locations',
'woocommerce_ship_to_destination',
'woocommerce_shipping_cost_requires_address',
'woocommerce_shipping_debug_mode',
'woocommerce_shipping_hide_rates_when_free',
'woocommerce-ppcp-data-payment', // This and the below options relate to the Pay with PayPal payments settings page. Required for the Activity Log.
'woocommerce-ppcp-data-settings',
'woocommerce_ppcp-applepay_settings',
'woocommerce_ppcp-axo-gateway_settings',
'woocommerce_ppcp-bancontact_settings',
'woocommerce_ppcp-blik_settings',
'woocommerce_ppcp-card-button-gateway_settings',
'woocommerce_ppcp-credit-card-gateway_settings',
'woocommerce_ppcp-eps_settings',
'woocommerce-ppcp-data-common',
'woocommerce-ppcp-data-onboarding',
'woocommerce_ppcp-googlepay_settings',
'woocommerce_ppcp-ideal_settings',
'woocommerce_ppcp-multibanco_settings',
'woocommerce_ppcp-mybank_settings',
'woocommerce_ppcp-oxxo-gateway_settings',
'woocommerce_ppcp-p24_settings',
'woocommerce_ppcp-pay-upon-invoice-gateway_settings',
'woocommerce_ppcp-pwc_settings',
'woocommerce_ppcp-trustly_settings',
'_wcpay_feature_customer_multi_currency', // This and the below options relate to WooPayments.
'current_protection_level',
'woocommerce_woocommerce_payments_apple_pay_settings',
'woocommerce_woocommerce_payments_google_pay_settings',
'woocommerce_woocommerce_payments_settings',
'wc_stripe_agentic_commerce_webhook_secret', // This and the below options relate to additional payment types.
'wc_square_settings',
'woocommerce_amazon_payments_advanced_settings',
'woocommerce_gift_cards_pay_settings',
'woocommerce_square_cash_app_pay_settings',
'woocommerce_square_credit_card_settings',
'woocommerce_stripe_settings',
'woocommerce_bacs_accounts', // This and the below options relate to offline payments.
'woocommerce_bacs_settings',
'woocommerce_cheque_settings',
'woocommerce_ppcp-recaptcha_settings', // This and the below options relate to the WooCommerce Integrations settings page. Required for the Activity Log.
'woocommerce_maxmind_geolocation_settings',
'woocommerce_store_pages_only', // This and the below options relate to the WooCommerce Site Visibility settings page. Required for the Activity Log.
'woocommerce_private_link',
'woocommerce_coming_soon',
'wcpay_multi_currency_enabled_currencies', // This and the below option relate to the WooCommerce Multi-Currency settings page. Required for the Activity Log.
'wcpay_multi_currency_enable_auto_currency',
'woocommerce_pos_store_name', // This and the below options relate to the WooCommerce Point of Sale settings page. Required for the Activity Log.
'woocommerce_pos_store_address',
'woocommerce_pos_store_phone',
'woocommerce_pos_store_email',
'woocommerce_pos_refund_returns_policy',
'wcs_notification_settings_update_time', // This and the below options relate to the WooCommerce Subscriptions settings page. Required for the Activity Log.
'wcsatt_add_cart_to_subscription',
'wcsatt_add_product_to_subscription',
'woocommerce_subscriptions_accept_manual_renewals',
'woocommerce_subscriptions_allow_switching',
'woocommerce_subscriptions_allow_switching_product_plans',
'woocommerce_subscriptions_apportion_length',
'woocommerce_subscriptions_apportion_recurring_price',
'woocommerce_subscriptions_apportion_sign_up_fee',
'woocommerce_subscriptions_cancelled_role',
'woocommerce_subscriptions_customer_notifications_enabled',
'woocommerce_subscriptions_customer_notifications_offset',
'woocommerce_subscriptions_downloads_add_line_items',
'woocommerce_subscriptions_drip_downloadable_content_on_renewal',
'woocommerce_subscriptions_enable_auto_renewal_toggle',
'woocommerce_subscriptions_enable_downloadable_file_linking',
'woocommerce_subscriptions_enable_early_renewal',
'woocommerce_subscriptions_enable_retry',
'woocommerce_subscriptions_enable_simple_subscription',
'woocommerce_subscriptions_enable_variable_subscription',
'woocommerce_subscriptions_first_billing_behavior',
'woocommerce_subscriptions_gifting_default_option',
'woocommerce_subscriptions_gifting_downloadable_products',
'woocommerce_subscriptions_gifting_enable_gifting',
'woocommerce_subscriptions_max_customer_suspensions',
'woocommerce_subscriptions_multiple_purchase',
'woocommerce_subscriptions_prorate_physical',
'woocommerce_subscriptions_subscriber_role',
'woocommerce_subscriptions_turn_off_automatic_payments',
'woocommerce_subscriptions_zero_initial_payment_requires_payment',
'woocommerce_email_from_address', // This and the below options relate to the WooCommerce Emails settings page. Required for the Activity Log.
'woocommerce_email_from_name',
'woocommerce_email_reply_to_address',
'woocommerce_email_reply_to_enabled',
'woocommerce_email_reply_to_name',
);
/**
* Whitelist for constants we are interested to sync.
*
* @access private
* @static
*
* @var array
*/
private static $wc_constants_whitelist = array(
// WooCommerce constants.
'WC_PLUGIN_FILE',
'WC_ABSPATH',
'WC_PLUGIN_BASENAME',
'WC_VERSION',
'WOOCOMMERCE_VERSION',
'WC_ROUNDING_PRECISION',
'WC_DISCOUNT_ROUNDING_MODE',
'WC_TAX_ROUNDING_MODE',
'WC_DELIMITER',
'WC_LOG_DIR',
'WC_SESSION_CACHE_GROUP',
'WC_TEMPLATE_DEBUG_MODE',
);
/**
* Whitelist for post meta we are interested to sync.
*
* @access private
* @static
*
* @var array
*/
public static $wc_post_meta_whitelist = array(
// WooCommerce products.
// See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-product-data-store-cpt.php#L21 .
'_visibility',
'_sku',
'_price',
'_regular_price',
'_sale_price',
'_sale_price_dates_from',
'_sale_price_dates_to',
'total_sales',
'_tax_status',
'_tax_class',
'_manage_stock',
'_backorders',
'_sold_individually',
'_weight',
'_length',
'_width',
'_height',
'_upsell_ids',
'_crosssell_ids',
'_purchase_note',
'_default_attributes',
'_product_attributes',
'_virtual',
'_downloadable',
'_download_limit',
'_download_expiry',
'_featured',
'_downloadable_files',
'_wc_rating_count',
'_wc_average_rating',
'_wc_review_count',
'_variation_description',
'_thumbnail_id',
'_file_paths',
'_product_image_gallery',
'_product_version',
'_wp_old_slug',
// Woocommerce orders.
// See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L27 .
'_order_key',
'_order_currency',
// '_billing_first_name', do not sync these as they contain personal data
// '_billing_last_name',
// '_billing_company',
// '_billing_address_1',
// '_billing_address_2',
'_billing_city',
'_billing_state',
'_billing_postcode',
'_billing_country',
// '_billing_email', do not sync these as they contain personal data.
// '_billing_phone',
// '_shipping_first_name',
// '_shipping_last_name',
// '_shipping_company',
// '_shipping_address_1',
// '_shipping_address_2',
'_shipping_city',
'_shipping_state',
'_shipping_postcode',
'_shipping_country',
'_completed_date',
'_paid_date',
'_cart_discount',
'_cart_discount_tax',
'_order_shipping',
'_order_shipping_tax',
'_order_tax',
'_order_total',
'_payment_method',
'_payment_method_title',
// '_transaction_id', do not sync these as they contain personal data.
// '_customer_ip_address',
// '_customer_user_agent',
'_created_via',
'_order_version',
'_prices_include_tax',
'_date_completed',
'_date_paid',
'_payment_tokens',
// '_billing_address_index', do not sync these as they contain personal data.
// '_shipping_address_index',
'_recorded_sales',
'_recorded_coupon_usage_counts',
// See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L539 .
'_download_permissions_granted',
// See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L594 .
'_order_stock_reduced',
'_cart_hash',
// Woocommerce order refunds.
// See https://github.com/woocommerce/woocommerce/blob/b8a2815ae546c836467008739e7ff5150cb08e93/includes/data-stores/class-wc-order-refund-data-store-cpt.php#L20 .
'_order_currency',
'_refund_amount',
'_refunded_by',
'_refund_reason',
'_order_shipping',
'_order_shipping_tax',
'_order_tax',
'_order_total',
'_order_version',
'_prices_include_tax',
'_payment_tokens',
);
/**
* Whitelist for comment meta we are interested to sync.
*
* @access private
* @static
*
* @var array
*/
private static $wc_comment_meta_whitelist = array(
'rating',
);
/**
* Return a list of objects by their type and IDs
*
* @param string $object_type Object type.
* @param array $ids IDs of objects to return.
*
* @access public
*
* @return array|object|WP_Error|null
*/
public function get_objects_by_id( $object_type, $ids ) {
switch ( $object_type ) {
case 'order_item':
return $this->get_order_item_by_ids( $ids );
}
return new WP_Error( 'unsupported_object_type', 'Unsupported object type' );
}
/**
* Returns a list of order_item objects by their IDs.
*
* @param array $ids List of order_item IDs to fetch.
* @param string $order Either 'ASC' or 'DESC'.
*
* @access public
*
* @return array|object|null
*/
public function get_order_item_by_ids( $ids, $order = '' ) {
global $wpdb;
if ( ! is_array( $ids ) ) {
return array();
}
// Make sure the IDs are numeric and are non-zero.
$ids = array_filter( array_map( 'intval', $ids ) );
if ( empty( $ids ) ) {
return array();
}
// Prepare the placeholders for the prepared query below.
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
$query = "SELECT * FROM {$this->order_item_table_name} WHERE order_item_id IN ( $placeholders )";
if ( ! empty( $order ) && in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
$query .= " ORDER BY order_item_id $order";
}
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return $wpdb->get_results( $wpdb->prepare( $query, $ids ), ARRAY_A );
}
/**
* Build the full sync action object for WooCommerce order items.
*
* @access public
*
* @param array $args An array with the order items and the previous end.
*
* @return array An array with the order items, order item meta and the previous end.
*/
public function build_full_sync_action_array( $args ) {
list( $filtered_order_items, $previous_end ) = $args;
return array(
'order_items' => $filtered_order_items['objects'],
'order_item_meta' => $filtered_order_items['meta'],
'previous_end' => $previous_end,
);
}
/**
* Given the Module Configuration and Status return the next chunk of items to send.
* This function also expands the posts and metadata and filters them based on the maximum size constraints.
*
* @param array $config This module Full Sync configuration.
* @param array $status This module Full Sync status.
* @param int $chunk_size Chunk size.
*
* @return array
*/
public function get_next_chunk( $config, $status, $chunk_size ) {
$order_item_ids = parent::get_next_chunk( $config, $status, $chunk_size );
if ( empty( $order_item_ids ) ) {
return array();
}
// Fetch the order items in DESC order for the next chunk logic to work.
$order_items = $this->get_order_item_by_ids( $order_item_ids, 'DESC' );
// If no orders were fetched, make sure to return the expected structure so that status is updated correctly.
if ( empty( $order_items ) ) {
return array(
'object_ids' => $order_item_ids,
'objects' => array(),
);
}
// Get the order IDs from the orders that were fetched.
$fetched_order_item_ids = wp_list_pluck( $order_items, 'order_item_id' );
$metadata = $this->get_metadata( $fetched_order_item_ids, 'order_item', static::$order_item_meta_whitelist );
// Filter the orders and metadata based on the maximum size constraints.
list( $filtered_order_item_ids, $filtered_order_items, $filtered_order_items_metadata ) = $this->filter_objects_and_metadata_by_size(
'order_item',
$order_items,
$metadata,
self::MAX_META_LENGTH,
self::MAX_SIZE_FULL_SYNC
);
return array(
'object_ids' => $filtered_order_item_ids,
'objects' => $filtered_order_items,
'meta' => $filtered_order_items_metadata,
);
}
}