PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-sync/src/modules/class-woocommerce.php +1372 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,1372 @@
1 +<?php
2 +/**
3 + * WooCommerce sync module.
4 + *
5 + * @package automattic/jetpack-sync
6 + */
7 +
8 +namespace Automattic\Jetpack\Sync\Modules;
9 +
10 +use WC_Order;
11 +use WP_Error;
12 +
13 +if ( ! defined( 'ABSPATH' ) ) {
14 + exit( 0 );
15 +}
16 +
17 +/**
18 + * Class to handle sync for WooCommerce.
19 + */
20 +class WooCommerce extends Module {
21 + /**
22 + * Whitelist for order item meta we are interested to sync.
23 + *
24 + * @access private
25 + *
26 + * @var array
27 + */
28 + public static $order_item_meta_whitelist = array(
29 + // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-product-store.php#L20 .
30 + '_product_id',
31 + '_variation_id',
32 + '_qty',
33 + // Tax ones also included in below class
34 + // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-fee-data-store.php#L20 .
35 + '_tax_class',
36 + '_tax_status',
37 + '_line_subtotal',
38 + '_line_subtotal_tax',
39 + '_line_total',
40 + '_line_tax',
41 + '_line_tax_data',
42 + // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-shipping-data-store.php#L20 .
43 + 'method_id',
44 + 'cost',
45 + 'total_tax',
46 + 'taxes',
47 + // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-tax-data-store.php#L20 .
48 + 'rate_id',
49 + 'label',
50 + 'compound',
51 + 'tax_amount',
52 + 'shipping_tax_amount',
53 + // See https://github.com/woocommerce/woocommerce/blob/master/includes/data-stores/class-wc-order-item-coupon-data-store.php .
54 + 'discount_amount',
55 + 'discount_amount_tax',
56 + );
57 +
58 + /**
59 + * Mapping between WooCommerce customer detail user meta keys and customer prop names.
60 + *
61 + * @access private
62 + *
63 + * @var array
64 + */
65 + private static $customer_detail_meta_key_to_prop = array(
66 + 'paying_customer' => 'is_paying_customer',
67 + 'billing_first_name' => 'billing_first_name',
68 + 'billing_last_name' => 'billing_last_name',
69 + 'billing_company' => 'billing_company',
70 + 'billing_address_1' => 'billing_address_1',
71 + 'billing_address_2' => 'billing_address_2',
72 + 'billing_city' => 'billing_city',
73 + 'billing_state' => 'billing_state',
74 + 'billing_postcode' => 'billing_postcode',
75 + 'billing_country' => 'billing_country',
76 + 'billing_email' => 'billing_email',
77 + 'billing_phone' => 'billing_phone',
78 + 'shipping_first_name' => 'shipping_first_name',
79 + 'shipping_last_name' => 'shipping_last_name',
80 + 'shipping_company' => 'shipping_company',
81 + 'shipping_address_1' => 'shipping_address_1',
82 + 'shipping_address_2' => 'shipping_address_2',
83 + 'shipping_city' => 'shipping_city',
84 + 'shipping_state' => 'shipping_state',
85 + 'shipping_postcode' => 'shipping_postcode',
86 + 'shipping_country' => 'shipping_country',
87 + 'shipping_phone' => 'shipping_phone',
88 + );
89 +
90 + /**
91 + * Name of the order item database table.
92 + *
93 + * @access private
94 + *
95 + * @var string
96 + */
97 + private $order_item_table_name;
98 +
99 + /**
100 + * Customer detail meta changes to sync at the end of the request.
101 + *
102 + * @var array
103 + */
104 + private $customer_meta_updates = array();
105 +
106 + /**
107 + * User IDs deleted during the current request.
108 + *
109 + * @var array
110 + */
111 + private $deleted_user_ids = array();
112 +
113 + /**
114 + * Order IDs whose total we've already emitted this request, so an order's total is emitted once
115 + * even if both woocommerce_new_order and woocommerce_order_status_changed observe it.
116 + *
117 + * @var array
118 + */
119 + private $synced_order_total_keys = array();
120 +
121 + /**
122 + * Cached list of order statuses WooCommerce considers paid, memoized per request to avoid
123 + * re-running wc_get_is_paid_statuses() (and its filters) on every order this request observes.
124 + *
125 + * @var array|null
126 + */
127 + private $paid_order_statuses = null;
128 +
129 + /**
130 + * The table name.
131 + *
132 + * @access public
133 + *
134 + * @return string
135 + * @deprecated since 3.11.0 Use table() instead.
136 + */
137 + public function table_name() {
138 + _deprecated_function( __METHOD__, '3.11.0', 'Automattic\\Jetpack\\Sync\\WooCommerce->table' );
139 + return $this->order_item_table_name;
140 + }
141 +
142 + /**
143 + * The table in the database with the prefix.
144 + *
145 + * @access public
146 + *
147 + * @return string|bool
148 + */
149 + public function table() {
150 + global $wpdb;
151 + return $wpdb->prefix . 'woocommerce_order_items';
152 + }
153 +
154 + /**
155 + * The id field in the database.
156 + *
157 + * @access public
158 + *
159 + * @return string
160 + */
161 + public function id_field() {
162 + return 'order_item_id';
163 + }
164 +
165 + /**
166 + * The full sync action name for this module.
167 + *
168 + * @access public
169 + *
170 + * @return string
171 + */
172 + public function full_sync_action_name() {
173 + return 'jetpack_full_sync_woocommerce_order_items';
174 + }
175 +
176 + /**
177 + * Constructor.
178 + *
179 + * @global $wpdb
180 + *
181 + * @todo Should we refactor this to use $this->set_defaults() instead?
182 + */
183 + public function __construct() {
184 + global $wpdb;
185 + $this->order_item_table_name = $wpdb->prefix . 'woocommerce_order_items';
186 +
187 + // Options, constants and post meta whitelists.
188 + add_filter( 'jetpack_sync_options_whitelist', array( $this, 'add_woocommerce_options_whitelist' ), 10 );
189 + add_filter( 'jetpack_sync_constants_whitelist', array( $this, 'add_woocommerce_constants_whitelist' ), 10 );
190 + add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'add_woocommerce_post_meta_whitelist' ), 10 );
191 + add_filter( 'jetpack_sync_comment_meta_whitelist', array( $this, 'add_woocommerce_comment_meta_whitelist' ), 10 );
192 +
193 + add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_order_item', array( $this, 'filter_order_item' ) );
194 + add_filter( 'jetpack_sync_before_enqueue_jetpack_updated_woo_customer_meta', array( $this, 'filter_customer_updated_meta' ) );
195 +
196 + // Append an order's total to these actions when it reaches a paid status.
197 + add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_order', array( $this, 'add_order_total_to_new_order' ) );
198 + add_filter( 'jetpack_sync_before_enqueue_woocommerce_order_status_changed', array( $this, 'add_order_total_to_status_changed' ) );
199 + add_filter( 'jetpack_sync_whitelisted_comment_types', array( $this, 'add_review_comment_types' ) );
200 +
201 + // Blacklist Action Scheduler comment types.
202 + add_filter( 'jetpack_sync_prevent_sending_comment_data', array( $this, 'filter_action_scheduler_comments' ), 10, 2 );
203 +
204 + // Preprocess action to be sent by Jetpack sync.
205 + add_action( 'woocommerce_remove_order_items', array( $this, 'action_woocommerce_remove_order_items' ), 10, 2 );
206 + }
207 +
208 + /**
209 + * Sync module name.
210 + *
211 + * @access public
212 + *
213 + * @return string
214 + */
215 + public function name() {
216 + return 'woocommerce';
217 + }
218 +
219 + /**
220 + * Initialize WooCommerce action listeners.
221 + *
222 + * @access public
223 + *
224 + * @param callable $callable Action handler callable.
225 + */
226 + public function init_listeners( $callable ) {
227 + // Attributes.
228 + add_action( 'woocommerce_attribute_added', $callable, 10, 2 );
229 + add_action( 'woocommerce_attribute_updated', $callable, 10, 3 );
230 + add_action( 'woocommerce_attribute_deleted', $callable, 10, 3 );
231 +
232 + // Orders. When an order reaches a paid status we append its total to these actions (via the
233 + // jetpack_sync_before_enqueue_* filters in the constructor) so the Activity Log can aggregate
234 + // revenue without a dedicated action. We register the extra accepted args so those filters
235 + // receive the order object WooCommerce already passes (2nd arg here, 4th for the status change)
236 + // and can avoid reloading it on this hot path; the filters strip the object back out before the
237 + // action is enqueued, so it is never serialized or sent to WPcom.
238 + add_action( 'woocommerce_new_order', $callable, 10, 2 );
239 + add_action( 'woocommerce_order_status_changed', $callable, 10, 4 );
240 + add_action( 'woocommerce_payment_complete', $callable, 10, 1 );
241 +
242 + // Order items.
243 + add_action( 'woocommerce_new_order_item', $callable, 10, 4 );
244 + add_action( 'woocommerce_delete_order_item', $callable, 10, 1 );
245 + add_action( 'woocommerce_remove_order_item_ids', $callable, 10, 1 );
246 + $this->init_listeners_for_meta_type( 'order_item', $callable );
247 + $this->init_meta_whitelist_handler( 'order_item', array( $this, 'filter_meta' ) );
248 +
249 + // Payment tokens.
250 + add_action( 'woocommerce_new_payment_token', $callable, 10, 1 );
251 + add_action( 'woocommerce_payment_token_deleted', $callable, 10, 2 );
252 + add_action( 'woocommerce_payment_token_updated', $callable, 10, 1 );
253 + $this->init_listeners_for_meta_type( 'payment_token', $callable );
254 +
255 + // Product downloads.
256 + add_action( 'woocommerce_downloadable_product_download_log_insert', $callable, 10, 1 );
257 + add_action( 'woocommerce_grant_product_download_access', $callable, 10, 1 );
258 +
259 + // Tax rates.
260 + // These are ignored on WP.com: tax items are derived from order data via wc_order_tax_lookup, which isn’t present there.
261 + add_action( 'woocommerce_tax_rate_added', $callable, 10, 2 );
262 + add_action( 'woocommerce_tax_rate_updated', $callable, 10, 2 );
263 + add_action( 'woocommerce_tax_rate_deleted', $callable, 10, 1 );
264 +
265 + // Webhooks.
266 + add_action( 'woocommerce_new_webhook', $callable, 10, 1 );
267 + add_action( 'woocommerce_webhook_deleted', $callable, 10, 2 );
268 + add_action( 'woocommerce_webhook_updated', $callable, 10, 1 );
269 +
270 + // Customers.
271 + add_action( 'added_user_meta', array( $this, 'maybe_sync_customer_meta_update' ), 10, 4 );
272 + add_action( 'updated_user_meta', array( $this, 'maybe_sync_customer_meta_update' ), 10, 4 );
273 + add_action( 'deleted_user_meta', array( $this, 'maybe_sync_customer_meta_update' ), 10, 4 );
274 + add_action( 'delete_user', array( $this, 'action_delete_user' ), 10, 1 );
275 + add_action( 'wpmu_delete_user', array( $this, 'action_delete_user' ), 10, 1 );
276 + add_action( 'shutdown', array( $this, 'action_customer_meta_updates' ) );
277 + add_action( 'jetpack_updated_woo_customer_meta', $callable, 10, 2 );
278 + }
279 +
280 + /**
281 + * Initialize WooCommerce action listeners for full sync.
282 + *
283 + * @access public
284 + *
285 + * @param callable $callable Action handler callable.
286 + */
287 + public function init_full_sync_listeners( $callable ) {
288 + add_action( 'jetpack_full_sync_woocommerce_order_items', $callable ); // Also sends post meta.
289 + }
290 +
291 + /**
292 + * Retrieve the actions that will be sent for this module during a full sync.
293 + *
294 + * @access public
295 + *
296 + * @return array Full sync actions of this module.
297 + */
298 + public function get_full_sync_actions() {
299 + return array( 'jetpack_full_sync_woocommerce_order_items' );
300 + }
301 +
302 + /**
303 + * Initialize the module in the sender.
304 + *
305 + * @access public
306 + */
307 + public function init_before_send() {
308 + // Full sync.
309 + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_order_items', array( $this, 'build_full_sync_action_array' ) );
310 + }
311 +
312 + /**
313 + * Expand the order items properly.
314 + *
315 + * @access public
316 + *
317 + * @param array $args The hook arguments.
318 + * @return array $args The hook arguments.
319 + */
320 + public function filter_order_item( $args ) {
321 + // 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.
322 + $args[1] = $this->build_order_item( $args[0] );
323 + return $args;
324 + }
325 +
326 + /**
327 + * Append an order's total to the synced woocommerce_new_order args when it is paid.
328 + *
329 + * A brand new order can be created already in a paid status, in which case no status transition
330 + * fires and only woocommerce_new_order observes the payment. When the order is paid we append a
331 + * trailing order-total payload (total, currency) that the Activity Log aggregates into
332 + * revenue; otherwise only the order ID is synced (the action still syncs for other purposes).
333 + *
334 + * @since 4.44.0 Appends a trailing [ 'total', 'currency' ] payload when the new order is paid.
335 + *
336 + * @param array $args Hook args: [ order_id, WC_Order ]. The order object is WooCommerce's 2nd arg.
337 + * @return array|false The args ( [ order_id ] ), with a trailing order-total payload appended when paid, or false when invalid.
338 + */
339 + public function add_order_total_to_new_order( $args ) {
340 + if ( ! is_array( $args ) || count( $args ) < 1 || ! is_numeric( $args[0] ) || (int) $args[0] <= 0 ) {
341 + return false;
342 + }
343 +
344 + $order_id = (int) $args[0];
345 +
346 + // Only use the order object WooCommerce passes as the 2nd arg; avoid wc_get_order on this hot path.
347 + $order = ( isset( $args[1] ) && $args[1] instanceof WC_Order ) ? $args[1] : null;
348 +
349 + // Rebuild the scalar arg shape WPcom expects. This also drops the WC_Order object the listener now
350 + // receives so it is never enqueued or serialized into the sync queue.
351 + $args = array( $order_id );
352 + if ( $order && $this->is_paid_order_status( $order->get_status() ) ) {
353 + $args = $this->maybe_append_order_total( $args, $order );
354 + }
355 +
356 + return $args;
357 + }
358 +
359 + /**
360 + * Append an order's total to the synced woocommerce_order_status_changed args on payment.
361 + *
362 + * We emit on the transition *into* a paid status from a non-paid one — the payment moment — and
363 + * skip paid -> paid steps (e.g. processing -> completed) so a fulfillment doesn't re-emit. When
364 + * emitted we append a trailing order-total payload (total, currency) the Activity Log
365 + * reads; otherwise only [ order_id, status_from, status_to ] is synced (the action still syncs for
366 + * other purposes).
367 + *
368 + * @since 4.44.0 Appends a trailing [ 'total', 'currency' ] payload on the paid transition.
369 + *
370 + * @param array $args Hook args: [ order_id, status_from, status_to, WC_Order ]. The order is the 4th arg.
371 + * @return array|false The args ( [ order_id, status_from, status_to ] ), with a trailing payload on the paid transition, or false when invalid.
372 + */
373 + public function add_order_total_to_status_changed( $args ) {
374 + if ( ! is_array( $args ) || count( $args ) < 3 || ! is_numeric( $args[0] ) || (int) $args[0] <= 0 ) {
375 + return false;
376 + }
377 +
378 + $order_id = (int) $args[0];
379 +
380 + $status_from = $args[1];
381 + $status_to = $args[2];
382 + if ( ! is_string( $status_from ) || ! is_string( $status_to ) ) {
383 + return false;
384 + }
385 +
386 + // Only use the order object WooCommerce passes as the 4th arg; avoid wc_get_order on this hot path.
387 + $order = ( isset( $args[3] ) && $args[3] instanceof WC_Order ) ? $args[3] : null;
388 +
389 + // Rebuild the scalar arg shape WPcom expects. This also drops the WC_Order object the listener now
390 + // receives so it is never enqueued or serialized into the sync queue.
391 + $args = array( $order_id, $status_from, $status_to );
392 +
393 + if ( $this->is_paid_order_status( $status_to ) && ! $this->is_paid_order_status( $status_from ) ) {
394 + $args = $this->maybe_append_order_total( $args, $order );
395 + }
396 +
397 + return $args;
398 + }
399 +
400 + /**
401 + * Append the order-total payload to the given args when this is the order's paid moment.
402 + *
403 + * @param array $args The scalar args built so far for the action.
404 + * @param WC_Order|null $order Order object, or null when WooCommerce did not pass one.
405 + * @return array The args, with a trailing order-total payload appended when emitted.
406 + */
407 + private function maybe_append_order_total( $args, $order ) {
408 + if ( $order && $this->claim_order_total_emission( $order ) ) {
409 + $payload = $this->build_order_total_payload( $order );
410 +
411 + if ( $payload !== null ) {
412 + $args[] = $payload;
413 + }
414 + }
415 +
416 + return $args;
417 + }
418 +
419 + /**
420 + * Claim the once-per-request emission slot for an order's total.
421 + *
422 + * Test-and-set: returns true (and records the claim) the first time it's called for an order this
423 + * request, false thereafter — so the woocommerce_new_order and woocommerce_order_status_changed
424 + * hooks don't both emit a freshly created paid order. Callers must confirm the order is paid first.
425 + *
426 + * @param WC_Order $order Order object.
427 + * @return bool True when the caller obtained the claim and should emit.
428 + */
429 + private function claim_order_total_emission( $order ) {
430 + $key = $order->get_id();
431 + if ( isset( $this->synced_order_total_keys[ $key ] ) ) {
432 + return false;
433 + }
434 + $this->synced_order_total_keys[ $key ] = true;
435 +
436 + return true;
437 + }
438 +
439 + /**
440 + * Build the trailing order-total payload appended to a paid order's synced action args.
441 + *
442 + * Intentionally minimal and scalar-only so it is safe to store and index on WPcom (Activity Log,
443 + * Elasticsearch, MCP integrations). We read with the 'edit' context to get the raw stored values and
444 + * skip the woocommerce_order_get_total / _currency view filters (e.g. multi-currency display
445 + * conversion), then still normalize the total to a numeric string and cast the currency rather than
446 + * trust whatever WooCommerce returns.
447 + *
448 + * @param WC_Order $order Order object.
449 + * @return null|array {
450 + * @type string $total Order total as a numeric string.
451 + * @type string $currency Order currency code (e.g. 'USD').
452 + * }
453 + */
454 + private function build_order_total_payload( $order ) {
455 + $total = $order->get_total( 'edit' );
456 +
457 + if ( $total <= 0 ) {
458 + return null;
459 + }
460 +
461 + return array(
462 + 'total' => function_exists( 'wc_format_decimal' ) ? wc_format_decimal( $total ) : (string) $total,
463 + 'currency' => (string) $order->get_currency( 'edit' ),
464 + );
465 + }
466 +
467 + /**
468 + * Whether an order status is one WooCommerce considers paid (and whose total we therefore sync).
469 + *
470 + * Uses WooCommerce's canonical, filterable list (wc_get_is_paid_statuses(), default 'processing'
471 + * and 'completed', un-prefixed) so stores that register custom paid statuses are covered.
472 + *
473 + * @param string $status Order status without the `wc-` prefix (e.g. 'processing').
474 + * @return bool True when WooCommerce treats the status as paid.
475 + */
476 + private function is_paid_order_status( $status ) {
477 + // Fail fast on empty/invalid input (e.g. a missing status arg) before the WooCommerce lookup.
478 + if ( ! is_string( $status ) || '' === $status || ! function_exists( 'wc_get_is_paid_statuses' ) ) {
479 + return false;
480 + }
481 +
482 + if ( null === $this->paid_order_statuses ) {
483 + $this->paid_order_statuses = wc_get_is_paid_statuses();
484 + }
485 +
486 + return in_array( $status, $this->paid_order_statuses, true );
487 + }
488 +
489 + /**
490 + * Validate the minimal customer meta update payload before enqueueing.
491 + *
492 + * @param array $args Hook arguments.
493 + * @return array|false Minimal user object and changed prop names, or false when invalid.
494 + */
495 + public function filter_customer_updated_meta( $args ) {
496 + if (
497 + ! is_array( $args )
498 + || ! isset( $args[0] )
499 + || ! isset( $args[1] )
500 + || ! is_object( $args[0] )
501 + || ! isset( $args[0]->data )
502 + || ! is_object( $args[0]->data )
503 + || ! isset( $args[0]->data->ID )
504 + || ! is_numeric( $args[0]->data->ID )
505 + || ! is_array( $args[1] )
506 + ) {
507 + return false;
508 + }
509 +
510 + $customer_id = (int) $args[0]->data->ID;
511 + if ( $customer_id <= 0 ) {
512 + return false;
513 + }
514 +
515 + $updated_props = $this->get_customer_detail_props( $args[1] );
516 + if ( empty( $updated_props ) ) {
517 + return false;
518 + }
519 +
520 + return array( $this->build_minimal_customer_user_object( $customer_id ), $updated_props );
521 + }
522 +
523 + /**
524 + * Track updated WooCommerce customer meta props for syncing.
525 + *
526 + * @param int|array $meta_id ID of the meta object, or IDs for deleted meta.
527 + * @param int $user_id User ID.
528 + * @param string $meta_key Meta key.
529 + * @param mixed $value Meta value.
530 + */
531 + public function maybe_sync_customer_meta_update( $meta_id, $user_id, $meta_key, $value ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
532 + $customer_id = (int) $user_id;
533 + if ( $customer_id <= 0 ) {
534 + return;
535 + }
536 +
537 + if ( 'deleted_user_meta' === current_action() && isset( $this->deleted_user_ids[ $customer_id ] ) ) {
538 + return;
539 + }
540 +
541 + if ( ! is_string( $meta_key ) && ! is_numeric( $meta_key ) ) {
542 + return;
543 + }
544 +
545 + $meta_key = sanitize_key( (string) $meta_key );
546 + if ( ! isset( self::$customer_detail_meta_key_to_prop[ $meta_key ] ) ) {
547 + return;
548 + }
549 +
550 + $updated_prop = self::$customer_detail_meta_key_to_prop[ $meta_key ];
551 +
552 + if ( ! isset( $this->customer_meta_updates[ $customer_id ] ) ) {
553 + $this->customer_meta_updates[ $customer_id ] = array();
554 + }
555 +
556 + $this->customer_meta_updates[ $customer_id ][ $updated_prop ] = true;
557 + }
558 +
559 + /**
560 + * Mark a deleted user so customer meta cleanup does not sync as profile changes.
561 + *
562 + * @param int $user_id User ID.
563 + */
564 + public function action_delete_user( $user_id ) {
565 + $customer_id = (int) $user_id;
566 + if ( $customer_id <= 0 ) {
567 + return;
568 + }
569 +
570 + $this->deleted_user_ids[ $customer_id ] = true;
571 + unset( $this->customer_meta_updates[ $customer_id ] );
572 + }
573 +
574 + /**
575 + * Send batched WooCommerce customer meta updates.
576 + */
577 + public function action_customer_meta_updates() {
578 + if ( empty( $this->customer_meta_updates ) ) {
579 + return;
580 + }
581 +
582 + $customer_meta_updates = $this->customer_meta_updates;
583 + $this->customer_meta_updates = array();
584 +
585 + foreach ( $customer_meta_updates as $customer_id => $updated_props ) {
586 + if ( isset( $this->deleted_user_ids[ (int) $customer_id ] ) ) {
587 + continue;
588 + }
589 +
590 + /**
591 + * Fires when WooCommerce customer details stored in user meta are updated.
592 + *
593 + * @param object $customer Minimal WP_User-shaped customer object.
594 + * @param array $updated_props Updated customer detail prop names.
595 + */
596 + do_action(
597 + 'jetpack_updated_woo_customer_meta',
598 + $this->build_minimal_customer_user_object( (int) $customer_id ),
599 + array_keys( $updated_props )
600 + );
601 + }
602 + }
603 +
604 + /**
605 + * Retrieve whitelisted WooCommerce customer detail props.
606 + *
607 + * @param array $props Customer detail meta keys or prop names.
608 + * @return array Customer detail prop names.
609 + */
610 + private function get_customer_detail_props( $props ) {
611 + $updated_props = array();
612 + foreach ( $props as $prop ) {
613 + if ( ! is_string( $prop ) && ! is_numeric( $prop ) ) {
614 + continue;
615 + }
616 +
617 + $prop = sanitize_key( (string) $prop );
618 + if ( isset( self::$customer_detail_meta_key_to_prop[ $prop ] ) ) {
619 + $updated_props[] = self::$customer_detail_meta_key_to_prop[ $prop ];
620 + continue;
621 + }
622 +
623 + if ( in_array( $prop, self::$customer_detail_meta_key_to_prop, true ) ) {
624 + $updated_props[] = $prop;
625 + }
626 + }
627 +
628 + return array_values( array_unique( $updated_props ) );
629 + }
630 +
631 + /**
632 + * Build a minimal WP_User-shaped object for Activity Log.
633 + *
634 + * @param int $customer_id Customer user ID.
635 + * @return object Minimal user object.
636 + */
637 + private function build_minimal_customer_user_object( $customer_id ) {
638 + $user_data = (object) array(
639 + 'ID' => $customer_id,
640 + 'display_name' => '',
641 + 'user_login' => '',
642 + 'user_email' => '',
643 + );
644 +
645 + $user = get_userdata( $customer_id );
646 + if ( $user ) {
647 + $user_data->display_name = (string) $user->display_name;
648 + $user_data->user_login = (string) $user->user_login;
649 + $user_data->user_email = (string) $user->user_email;
650 + }
651 +
652 + return (object) array(
653 + 'ID' => $customer_id,
654 + 'data' => $user_data,
655 + );
656 + }
657 +
658 + /**
659 + * Handler for filtering out non-whitelisted order item meta.
660 + *
661 + * @since 4.22.3
662 + *
663 + * @param array $args Hook arguments.
664 + * @return array|false False if not whitelisted, the original hook args otherwise.
665 + */
666 + public function filter_meta( $args ) {
667 + if (
668 + ! empty( $args[2] ) && $this->is_whitelisted_order_item_meta( $args[2] )
669 + ) {
670 + return $args;
671 + }
672 +
673 + return false;
674 + }
675 +
676 + /**
677 + * Whether an order item meta key is whitelisted for sync.
678 + *
679 + * @access public
680 + *
681 + * @since 4.22.3
682 + *
683 + * @param string $meta_key Order item meta key.
684 + * @return bool True if whitelisted.
685 + */
686 + public function is_whitelisted_order_item_meta( $meta_key ) {
687 + return is_string( $meta_key ) && in_array( $meta_key, self::$order_item_meta_whitelist, true );
688 + }
689 +
690 + /**
691 + * Retrieve the order item ids to be removed and send them as one action
692 + *
693 + * @param WC_Order $order The order argument.
694 + * @param string $type Order item type.
695 + */
696 + public function action_woocommerce_remove_order_items( WC_Order $order, $type ) {
697 + if ( $type ) {
698 + $order_items = $order->get_items( $type );
699 + } else {
700 + $order_items = $order->get_items();
701 + }
702 + $order_item_ids = array_keys( $order_items );
703 +
704 + if ( $order_item_ids ) {
705 + do_action( 'woocommerce_remove_order_item_ids', $order_item_ids );
706 + }
707 + }
708 +
709 + /**
710 + * Expand order item IDs to order items and their meta.
711 + *
712 + * @access public
713 + *
714 + * @todo Refactor table name to use a $wpdb->prepare placeholder.
715 + *
716 + * @param array $args The hook arguments.
717 + * @return array $args Expanded order items with meta.
718 + * @deprecated since 4.7.0
719 + */
720 + public function expand_order_item_ids( $args ) {
721 + _deprecated_function( __METHOD__, '4.7.0' );
722 + $order_item_ids = $args[0];
723 +
724 + global $wpdb;
725 +
726 + $order_item_ids_sql = implode( ', ', array_map( 'intval', $order_item_ids ) );
727 +
728 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
729 + $order_items = $wpdb->get_results(
730 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
731 + "SELECT * FROM $this->order_item_table_name WHERE order_item_id IN ( $order_item_ids_sql )"
732 + );
733 +
734 + return array(
735 + $order_items,
736 + $this->get_metadata( $order_item_ids, 'order_item', static::$order_item_meta_whitelist ),
737 + );
738 + }
739 + /**
740 + * Extract the full order item from the database by its ID.
741 + *
742 + * @access public
743 + *
744 + * @param int $order_item_id Order item ID.
745 + * @return object Order item.
746 + */
747 + public function build_order_item( $order_item_id ) {
748 + global $wpdb;
749 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct database access is intentional; caching is not required for this query.
750 + return $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE order_item_id = %d', $this->order_item_table_name, $order_item_id ) );
751 + }
752 +
753 + /**
754 + * Enqueue the WooCommerce actions for full sync.
755 + *
756 + * @access public
757 + *
758 + * @param array $config Full sync configuration for this sync module.
759 + * @param int $max_items_to_enqueue Maximum number of items to enqueue.
760 + * @param boolean $state True if full sync has finished enqueueing this module, false otherwise.
761 + * @return array Number of actions enqueued, and next module state.
762 + */
763 + public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
764 + 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 );
765 + }
766 +
767 + /**
768 + * Retrieve an estimated number of actions that will be enqueued.
769 + *
770 + * @access public
771 + *
772 + * @todo Refactor the SQL query to use $wpdb->prepare().
773 + *
774 + * @param array $config Full sync configuration for this sync module.
775 + * @return int Number of items yet to be enqueued.
776 + */
777 + public function estimate_full_sync_actions( $config ) {
778 + global $wpdb;
779 +
780 + $query = "SELECT count(*) FROM $this->order_item_table_name WHERE " . $this->get_where_sql( $config );
781 + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
782 + $count = (int) $wpdb->get_var( $query );
783 +
784 + return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
785 + }
786 +
787 + /**
788 + * Retrieve the WHERE SQL clause based on the module config.
789 + *
790 + * @access private
791 + *
792 + * @param array $config Full sync configuration for this sync module.
793 + * @return string WHERE SQL clause.
794 + */
795 + public function get_where_sql( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
796 + return '1=1';
797 + }
798 +
799 + /**
800 + * Add WooCommerce options to the options whitelist.
801 + *
802 + * @param array $list Existing options whitelist.
803 + * @return array Updated options whitelist.
804 + */
805 + public function add_woocommerce_options_whitelist( $list ) {
806 + return array_values( array_unique( array_merge( $list, self::$wc_options_whitelist ) ) );
807 + }
808 +
809 + /**
810 + * Add WooCommerce constants to the constants whitelist.
811 + *
812 + * @param array $list Existing constants whitelist.
813 + * @return array Updated constants whitelist.
814 + */
815 + public function add_woocommerce_constants_whitelist( $list ) {
816 + return array_merge( $list, self::$wc_constants_whitelist );
817 + }
818 +
819 + /**
820 + * Add WooCommerce post meta to the post meta whitelist.
821 + *
822 + * @param array $list Existing post meta whitelist.
823 + * @return array Updated post meta whitelist.
824 + */
825 + public function add_woocommerce_post_meta_whitelist( $list ) {
826 + return array_merge( $list, self::$wc_post_meta_whitelist );
827 + }
828 +
829 + /**
830 + * Add WooCommerce comment meta to the comment meta whitelist.
831 + *
832 + * @param array $list Existing comment meta whitelist.
833 + * @return array Updated comment meta whitelist.
834 + */
835 + public function add_woocommerce_comment_meta_whitelist( $list ) {
836 + return array_merge( $list, self::$wc_comment_meta_whitelist );
837 + }
838 +
839 + /**
840 + * Adds 'revew' to the list of comment types so Sync will listen for status changes on 'reviews'.
841 + *
842 + * @access public
843 + *
844 + * @param array $comment_types The list of comment types prior to this filter.
845 + * return array The list of comment types with 'review' added.
846 + */
847 + public function add_review_comment_types( $comment_types ) {
848 + if ( is_array( $comment_types ) ) {
849 + $comment_types[] = 'review';
850 + }
851 + return $comment_types;
852 + }
853 +
854 + /**
855 + * Stop comments from the Action Scheduler from being synced.
856 + * https://github.com/woocommerce/woocommerce/tree/e7762627c37ec1f7590e6cac4218ba0c6a20024d/includes/libraries/action-scheduler
857 + *
858 + * @since 1.6.3
859 + * @since-jetpack 7.7.0
860 + *
861 + * @param boolean $can_sync Should we prevent comment data from bing synced to WordPress.com.
862 + * @param mixed $comment WP_COMMENT object.
863 + *
864 + * @return bool
865 + */
866 + public function filter_action_scheduler_comments( $can_sync, $comment ) {
867 + if ( isset( $comment->comment_agent ) && 'ActionScheduler' === $comment->comment_agent ) {
868 + return true;
869 + }
870 + return $can_sync;
871 + }
872 +
873 + /**
874 + * Whitelist for options we are interested to sync.
875 + *
876 + * @access private
877 + * @static
878 + *
879 + * @var array
880 + */
881 + private static $wc_options_whitelist = array(
882 + 'woocommerce_currency',
883 + 'woocommerce_db_version',
884 + 'woocommerce_weight_unit',
885 + 'woocommerce_version',
886 + 'woocommerce_unforce_ssl_checkout',
887 + 'woocommerce_tax_total_display',
888 + 'woocommerce_tax_round_at_subtotal',
889 + 'woocommerce_tax_display_shop',
890 + 'woocommerce_tax_display_cart',
891 + 'woocommerce_prices_include_tax',
892 + 'woocommerce_price_thousand_sep',
893 + 'woocommerce_price_num_decimals',
894 + 'woocommerce_price_decimal_sep',
895 + 'woocommerce_notify_low_stock',
896 + 'woocommerce_notify_low_stock_amount',
897 + 'woocommerce_notify_no_stock',
898 + 'woocommerce_notify_no_stock_amount',
899 + 'woocommerce_manage_stock',
900 + 'woocommerce_force_ssl_checkout',
901 + 'woocommerce_hide_out_of_stock_items',
902 + 'woocommerce_file_download_method',
903 + 'woocommerce_enable_signup_and_login_from_checkout',
904 + 'woocommerce_enable_shipping_calc',
905 + 'woocommerce_enable_review_rating',
906 + 'woocommerce_enable_guest_checkout',
907 + 'woocommerce_enable_coupons',
908 + 'woocommerce_enable_checkout_login_reminder',
909 + 'woocommerce_enable_ajax_add_to_cart',
910 + 'woocommerce_dimension_unit',
911 + 'woocommerce_default_country',
912 + 'woocommerce_default_customer_address',
913 + 'woocommerce_currency_pos',
914 + 'woocommerce_api_enabled',
915 + 'woocommerce_allow_tracking',
916 + 'woocommerce_task_list_hidden',
917 + 'woocommerce_cod_settings',
918 + 'woocommerce_store_address',
919 + 'woocommerce_store_address_2',
920 + 'woocommerce_store_city',
921 + 'woocommerce_store_postcode',
922 + 'woocommerce_admin_install_timestamp',
923 + '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.
924 + 'woocommerce_enable_myaccount_registration',
925 + 'woocommerce_registration_generate_password',
926 + 'woocommerce_erasure_request_removes_order_data',
927 + 'woocommerce_erasure_request_removes_subscription_data',
928 + 'woocommerce_erasure_request_removes_download_data',
929 + 'woocommerce_allow_bulk_remove_personal_data',
930 + 'woocommerce_registration_privacy_policy_text',
931 + 'woocommerce_checkout_privacy_policy_text',
932 + 'woocommerce_delete_inactive_accounts',
933 + 'woocommerce_trash_pending_orders',
934 + 'woocommerce_trash_failed_orders',
935 + 'woocommerce_trash_cancelled_orders',
936 + 'woocommerce_anonymize_refunded_orders',
937 + 'woocommerce_anonymize_completed_orders',
938 + 'woocommerce_anonymize_ended_subscriptions',
939 + 'woocommerce_enable_delayed_account_creation',
940 + 'woocommerce_gateway_stripe_retention',
941 + 'wc_downloads_approved_directories_mode', // This and the below options relate to the WooCommerce Products settings page. Required for the Activity Log.
942 + 'woocommerce_attribute_lookup_direct_updates',
943 + 'woocommerce_attribute_lookup_enabled',
944 + 'woocommerce_attribute_lookup_optimized_updates',
945 + 'woocommerce_cart_redirect_after_add',
946 + 'woocommerce_downloads_add_hash_to_filename',
947 + 'woocommerce_downloads_count_partial',
948 + 'woocommerce_downloads_deliver_inline',
949 + 'woocommerce_downloads_grant_access_after_payment',
950 + 'woocommerce_downloads_redirect_fallback_allowed',
951 + 'woocommerce_downloads_require_login',
952 + 'woocommerce_enable_reviews',
953 + 'woocommerce_hold_stock_minutes',
954 + 'woocommerce_review_rating_required',
955 + 'woocommerce_review_rating_verification_label',
956 + 'woocommerce_review_rating_verification_required',
957 + 'woocommerce_shop_page_id',
958 + 'woocommerce_stock_email_recipient',
959 + 'woocommerce_stock_format',
960 + 'woocommerce_allowed_countries', // This and the below options relate to the WooCommerce General settings page. Required for the Activity Log.
961 + 'woocommerce_specific_allowed_countries',
962 + 'woocommerce_ship_to_countries',
963 + 'woocommerce_specific_ship_to_countries',
964 + 'woocommerce_all_except_countries',
965 + 'woocommerce_calc_taxes',
966 + 'woocommerce_calc_discounts_sequentially',
967 + 'woocommerce_analytics_enabled', // This and the below options relate to the WooCommerce Advanced settings page. Required for the Activity Log.
968 + 'woocommerce_cart_page_id',
969 + 'woocommerce_checkout_order_received_endpoint',
970 + 'woocommerce_checkout_page_id',
971 + 'woocommerce_checkout_pay_endpoint',
972 + 'woocommerce_custom_orders_table_data_sync_enabled',
973 + 'woocommerce_custom_orders_table_enabled',
974 + 'woocommerce_date_type',
975 + 'woocommerce_feature_block_email_editor_enabled',
976 + 'woocommerce_feature_blueprint_enabled',
977 + 'woocommerce_feature_cost_of_goods_sold_enabled',
978 + 'woocommerce_feature_customer_review_request_enabled',
979 + 'woocommerce_feature_deferred_transactional_emails_enabled',
980 + 'woocommerce_feature_destroy-empty-sessions_enabled',
981 + 'woocommerce_feature_email_improvements_enabled',
982 + 'woocommerce_feature_mcp_integration_enabled',
983 + 'woocommerce_feature_order_attribution_enabled',
984 + 'woocommerce_feature_point_of_sale_enabled',
985 + 'woocommerce_feature_product_instance_caching_enabled',
986 + 'woocommerce_feature_rate_limit_checkout_enabled',
987 + 'woocommerce_feature_remote_logging_enabled',
988 + 'woocommerce_feature_rest_api_caching_enabled',
989 + 'woocommerce_feature_site_visibility_badge_enabled',
990 + 'woocommerce_hpos_datastore_caching_enabled',
991 + 'woocommerce_hpos_fts_index_enabled',
992 + 'woocommerce_logout_endpoint',
993 + 'woocommerce_myaccount_add_payment_method_endpoint',
994 + 'woocommerce_myaccount_delete_payment_method_endpoint',
995 + 'woocommerce_myaccount_downloads_endpoint',
996 + 'woocommerce_myaccount_edit_account_endpoint',
997 + 'woocommerce_myaccount_edit_address_endpoint',
998 + 'woocommerce_myaccount_lost_password_endpoint',
999 + 'woocommerce_myaccount_orders_endpoint',
1000 + 'woocommerce_myaccount_page_id',
1001 + 'woocommerce_myaccount_payment_methods_endpoint',
1002 + 'woocommerce_myaccount_set_default_payment_method_endpoint',
1003 + 'woocommerce_myaccount_subscription_payment_method_endpoint',
1004 + 'woocommerce_myaccount_subscriptions_endpoint',
1005 + 'woocommerce_myaccount_view_order_endpoint',
1006 + 'woocommerce_myaccount_view_subscription_endpoint',
1007 + 'woocommerce_show_marketplace_suggestions',
1008 + 'woocommerce_terms_page_id',
1009 + 'woocommerce_pickup_location_settings', // This and the below options relate to the WooCommerce Shipping settings page. Required for the Activity Log.
1010 + 'pickup_location_pickup_locations',
1011 + 'woocommerce_ship_to_destination',
1012 + 'woocommerce_shipping_cost_requires_address',
1013 + 'woocommerce_shipping_debug_mode',
1014 + 'woocommerce_shipping_hide_rates_when_free',
1015 + 'woocommerce-ppcp-data-payment', // This and the below options relate to the Pay with PayPal payments settings page. Required for the Activity Log.
1016 + 'woocommerce-ppcp-data-settings',
1017 + 'woocommerce_ppcp-applepay_settings',
1018 + 'woocommerce_ppcp-axo-gateway_settings',
1019 + 'woocommerce_ppcp-bancontact_settings',
1020 + 'woocommerce_ppcp-blik_settings',
1021 + 'woocommerce_ppcp-card-button-gateway_settings',
1022 + 'woocommerce_ppcp-credit-card-gateway_settings',
1023 + 'woocommerce_ppcp-eps_settings',
1024 + 'woocommerce-ppcp-data-common',
1025 + 'woocommerce-ppcp-data-onboarding',
1026 + 'woocommerce_ppcp-googlepay_settings',
1027 + 'woocommerce_ppcp-ideal_settings',
1028 + 'woocommerce_ppcp-multibanco_settings',
1029 + 'woocommerce_ppcp-mybank_settings',
1030 + 'woocommerce_ppcp-oxxo-gateway_settings',
1031 + 'woocommerce_ppcp-p24_settings',
1032 + 'woocommerce_ppcp-pay-upon-invoice-gateway_settings',
1033 + 'woocommerce_ppcp-pwc_settings',
1034 + 'woocommerce_ppcp-trustly_settings',
1035 + '_wcpay_feature_customer_multi_currency', // This and the below options relate to WooPayments.
1036 + 'current_protection_level',
1037 + 'woocommerce_woocommerce_payments_apple_pay_settings',
1038 + 'woocommerce_woocommerce_payments_google_pay_settings',
1039 + 'woocommerce_woocommerce_payments_settings',
1040 + 'wc_stripe_agentic_commerce_webhook_secret', // This and the below options relate to additional payment types.
1041 + 'wc_square_settings',
1042 + 'woocommerce_amazon_payments_advanced_settings',
1043 + 'woocommerce_gift_cards_pay_settings',
1044 + 'woocommerce_square_cash_app_pay_settings',
1045 + 'woocommerce_square_credit_card_settings',
1046 + 'woocommerce_stripe_settings',
1047 + 'woocommerce_bacs_accounts', // This and the below options relate to offline payments.
1048 + 'woocommerce_bacs_settings',
1049 + 'woocommerce_cheque_settings',
1050 + 'woocommerce_ppcp-recaptcha_settings', // This and the below options relate to the WooCommerce Integrations settings page. Required for the Activity Log.
1051 + 'woocommerce_maxmind_geolocation_settings',
1052 + 'woocommerce_store_pages_only', // This and the below options relate to the WooCommerce Site Visibility settings page. Required for the Activity Log.
1053 + 'woocommerce_private_link',
1054 + 'woocommerce_coming_soon',
1055 + 'wcpay_multi_currency_enabled_currencies', // This and the below option relate to the WooCommerce Multi-Currency settings page. Required for the Activity Log.
1056 + 'wcpay_multi_currency_enable_auto_currency',
1057 + 'woocommerce_pos_store_name', // This and the below options relate to the WooCommerce Point of Sale settings page. Required for the Activity Log.
1058 + 'woocommerce_pos_store_address',
1059 + 'woocommerce_pos_store_phone',
1060 + 'woocommerce_pos_store_email',
1061 + 'woocommerce_pos_refund_returns_policy',
1062 + 'wcs_notification_settings_update_time', // This and the below options relate to the WooCommerce Subscriptions settings page. Required for the Activity Log.
1063 + 'wcsatt_add_cart_to_subscription',
1064 + 'wcsatt_add_product_to_subscription',
1065 + 'woocommerce_subscriptions_accept_manual_renewals',
1066 + 'woocommerce_subscriptions_allow_switching',
1067 + 'woocommerce_subscriptions_allow_switching_product_plans',
1068 + 'woocommerce_subscriptions_apportion_length',
1069 + 'woocommerce_subscriptions_apportion_recurring_price',
1070 + 'woocommerce_subscriptions_apportion_sign_up_fee',
1071 + 'woocommerce_subscriptions_cancelled_role',
1072 + 'woocommerce_subscriptions_customer_notifications_enabled',
1073 + 'woocommerce_subscriptions_customer_notifications_offset',
1074 + 'woocommerce_subscriptions_downloads_add_line_items',
1075 + 'woocommerce_subscriptions_drip_downloadable_content_on_renewal',
1076 + 'woocommerce_subscriptions_enable_auto_renewal_toggle',
1077 + 'woocommerce_subscriptions_enable_downloadable_file_linking',
1078 + 'woocommerce_subscriptions_enable_early_renewal',
1079 + 'woocommerce_subscriptions_enable_retry',
1080 + 'woocommerce_subscriptions_enable_simple_subscription',
1081 + 'woocommerce_subscriptions_enable_variable_subscription',
1082 + 'woocommerce_subscriptions_first_billing_behavior',
1083 + 'woocommerce_subscriptions_gifting_default_option',
1084 + 'woocommerce_subscriptions_gifting_downloadable_products',
1085 + 'woocommerce_subscriptions_gifting_enable_gifting',
1086 + 'woocommerce_subscriptions_max_customer_suspensions',
1087 + 'woocommerce_subscriptions_multiple_purchase',
1088 + 'woocommerce_subscriptions_prorate_physical',
1089 + 'woocommerce_subscriptions_subscriber_role',
1090 + 'woocommerce_subscriptions_turn_off_automatic_payments',
1091 + 'woocommerce_subscriptions_zero_initial_payment_requires_payment',
1092 + 'woocommerce_email_from_address', // This and the below options relate to the WooCommerce Emails settings page. Required for the Activity Log.
1093 + 'woocommerce_email_from_name',
1094 + 'woocommerce_email_reply_to_address',
1095 + 'woocommerce_email_reply_to_enabled',
1096 + 'woocommerce_email_reply_to_name',
1097 + );
1098 +
1099 + /**
1100 + * Whitelist for constants we are interested to sync.
1101 + *
1102 + * @access private
1103 + * @static
1104 + *
1105 + * @var array
1106 + */
1107 + private static $wc_constants_whitelist = array(
1108 + // WooCommerce constants.
1109 + 'WC_PLUGIN_FILE',
1110 + 'WC_ABSPATH',
1111 + 'WC_PLUGIN_BASENAME',
1112 + 'WC_VERSION',
1113 + 'WOOCOMMERCE_VERSION',
1114 + 'WC_ROUNDING_PRECISION',
1115 + 'WC_DISCOUNT_ROUNDING_MODE',
1116 + 'WC_TAX_ROUNDING_MODE',
1117 + 'WC_DELIMITER',
1118 + 'WC_LOG_DIR',
1119 + 'WC_SESSION_CACHE_GROUP',
1120 + 'WC_TEMPLATE_DEBUG_MODE',
1121 + );
1122 +
1123 + /**
1124 + * Whitelist for post meta we are interested to sync.
1125 + *
1126 + * @access private
1127 + * @static
1128 + *
1129 + * @var array
1130 + */
1131 + public static $wc_post_meta_whitelist = array(
1132 + // WooCommerce products.
1133 + // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-product-data-store-cpt.php#L21 .
1134 + '_visibility',
1135 + '_sku',
1136 + '_price',
1137 + '_regular_price',
1138 + '_sale_price',
1139 + '_sale_price_dates_from',
1140 + '_sale_price_dates_to',
1141 + 'total_sales',
1142 + '_tax_status',
1143 + '_tax_class',
1144 + '_manage_stock',
1145 + '_backorders',
1146 + '_sold_individually',
1147 + '_weight',
1148 + '_length',
1149 + '_width',
1150 + '_height',
1151 + '_upsell_ids',
1152 + '_crosssell_ids',
1153 + '_purchase_note',
1154 + '_default_attributes',
1155 + '_product_attributes',
1156 + '_virtual',
1157 + '_downloadable',
1158 + '_download_limit',
1159 + '_download_expiry',
1160 + '_featured',
1161 + '_downloadable_files',
1162 + '_wc_rating_count',
1163 + '_wc_average_rating',
1164 + '_wc_review_count',
1165 + '_variation_description',
1166 + '_thumbnail_id',
1167 + '_file_paths',
1168 + '_product_image_gallery',
1169 + '_product_version',
1170 + '_wp_old_slug',
1171 +
1172 + // Woocommerce orders.
1173 + // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L27 .
1174 + '_order_key',
1175 + '_order_currency',
1176 + // '_billing_first_name', do not sync these as they contain personal data
1177 + // '_billing_last_name',
1178 + // '_billing_company',
1179 + // '_billing_address_1',
1180 + // '_billing_address_2',
1181 + '_billing_city',
1182 + '_billing_state',
1183 + '_billing_postcode',
1184 + '_billing_country',
1185 + // '_billing_email', do not sync these as they contain personal data.
1186 + // '_billing_phone',
1187 + // '_shipping_first_name',
1188 + // '_shipping_last_name',
1189 + // '_shipping_company',
1190 + // '_shipping_address_1',
1191 + // '_shipping_address_2',
1192 + '_shipping_city',
1193 + '_shipping_state',
1194 + '_shipping_postcode',
1195 + '_shipping_country',
1196 + '_completed_date',
1197 + '_paid_date',
1198 + '_cart_discount',
1199 + '_cart_discount_tax',
1200 + '_order_shipping',
1201 + '_order_shipping_tax',
1202 + '_order_tax',
1203 + '_order_total',
1204 + '_payment_method',
1205 + '_payment_method_title',
1206 + // '_transaction_id', do not sync these as they contain personal data.
1207 + // '_customer_ip_address',
1208 + // '_customer_user_agent',
1209 + '_created_via',
1210 + '_order_version',
1211 + '_prices_include_tax',
1212 + '_date_completed',
1213 + '_date_paid',
1214 + '_payment_tokens',
1215 + // '_billing_address_index', do not sync these as they contain personal data.
1216 + // '_shipping_address_index',
1217 + '_recorded_sales',
1218 + '_recorded_coupon_usage_counts',
1219 + // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L539 .
1220 + '_download_permissions_granted',
1221 + // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L594 .
1222 + '_order_stock_reduced',
1223 + '_cart_hash',
1224 +
1225 + // Woocommerce order refunds.
1226 + // See https://github.com/woocommerce/woocommerce/blob/b8a2815ae546c836467008739e7ff5150cb08e93/includes/data-stores/class-wc-order-refund-data-store-cpt.php#L20 .
1227 + '_order_currency',
1228 + '_refund_amount',
1229 + '_refunded_by',
1230 + '_refund_reason',
1231 + '_order_shipping',
1232 + '_order_shipping_tax',
1233 + '_order_tax',
1234 + '_order_total',
1235 + '_order_version',
1236 + '_prices_include_tax',
1237 + '_payment_tokens',
1238 + );
1239 +
1240 + /**
1241 + * Whitelist for comment meta we are interested to sync.
1242 + *
1243 + * @access private
1244 + * @static
1245 + *
1246 + * @var array
1247 + */
1248 + private static $wc_comment_meta_whitelist = array(
1249 + 'rating',
1250 + );
1251 +
1252 + /**
1253 + * Return a list of objects by their type and IDs
1254 + *
1255 + * @param string $object_type Object type.
1256 + * @param array $ids IDs of objects to return.
1257 + *
1258 + * @access public
1259 + *
1260 + * @return array|object|WP_Error|null
1261 + */
1262 + public function get_objects_by_id( $object_type, $ids ) {
1263 + switch ( $object_type ) {
1264 + case 'order_item':
1265 + return $this->get_order_item_by_ids( $ids );
1266 + }
1267 +
1268 + return new WP_Error( 'unsupported_object_type', 'Unsupported object type' );
1269 + }
1270 +
1271 + /**
1272 + * Returns a list of order_item objects by their IDs.
1273 + *
1274 + * @param array $ids List of order_item IDs to fetch.
1275 + * @param string $order Either 'ASC' or 'DESC'.
1276 + *
1277 + * @access public
1278 + *
1279 + * @return array|object|null
1280 + */
1281 + public function get_order_item_by_ids( $ids, $order = '' ) {
1282 + global $wpdb;
1283 +
1284 + if ( ! is_array( $ids ) ) {
1285 + return array();
1286 + }
1287 +
1288 + // Make sure the IDs are numeric and are non-zero.
1289 + $ids = array_filter( array_map( 'intval', $ids ) );
1290 +
1291 + if ( empty( $ids ) ) {
1292 + return array();
1293 + }
1294 +
1295 + // Prepare the placeholders for the prepared query below.
1296 + $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
1297 +
1298 + $query = "SELECT * FROM {$this->order_item_table_name} WHERE order_item_id IN ( $placeholders )";
1299 + if ( ! empty( $order ) && in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
1300 + $query .= " ORDER BY order_item_id $order";
1301 + }
1302 +
1303 + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
1304 + return $wpdb->get_results( $wpdb->prepare( $query, $ids ), ARRAY_A );
1305 + }
1306 +
1307 + /**
1308 + * Build the full sync action object for WooCommerce order items.
1309 + *
1310 + * @access public
1311 + *
1312 + * @param array $args An array with the order items and the previous end.
1313 + *
1314 + * @return array An array with the order items, order item meta and the previous end.
1315 + */
1316 + public function build_full_sync_action_array( $args ) {
1317 + list( $filtered_order_items, $previous_end ) = $args;
1318 + return array(
1319 + 'order_items' => $filtered_order_items['objects'],
1320 + 'order_item_meta' => $filtered_order_items['meta'],
1321 + 'previous_end' => $previous_end,
1322 + );
1323 + }
1324 +
1325 + /**
1326 + * Given the Module Configuration and Status return the next chunk of items to send.
1327 + * This function also expands the posts and metadata and filters them based on the maximum size constraints.
1328 + *
1329 + * @param array $config This module Full Sync configuration.
1330 + * @param array $status This module Full Sync status.
1331 + * @param int $chunk_size Chunk size.
1332 + *
1333 + * @return array
1334 + */
1335 + public function get_next_chunk( $config, $status, $chunk_size ) {
1336 +
1337 + $order_item_ids = parent::get_next_chunk( $config, $status, $chunk_size );
1338 +
1339 + if ( empty( $order_item_ids ) ) {
1340 + return array();
1341 + }
1342 + // Fetch the order items in DESC order for the next chunk logic to work.
1343 + $order_items = $this->get_order_item_by_ids( $order_item_ids, 'DESC' );
1344 +
1345 + // If no orders were fetched, make sure to return the expected structure so that status is updated correctly.
1346 + if ( empty( $order_items ) ) {
1347 + return array(
1348 + 'object_ids' => $order_item_ids,
1349 + 'objects' => array(),
1350 + );
1351 + }
1352 +
1353 + // Get the order IDs from the orders that were fetched.
1354 + $fetched_order_item_ids = wp_list_pluck( $order_items, 'order_item_id' );
1355 + $metadata = $this->get_metadata( $fetched_order_item_ids, 'order_item', static::$order_item_meta_whitelist );
1356 +
1357 + // Filter the orders and metadata based on the maximum size constraints.
1358 + list( $filtered_order_item_ids, $filtered_order_items, $filtered_order_items_metadata ) = $this->filter_objects_and_metadata_by_size(
1359 + 'order_item',
1360 + $order_items,
1361 + $metadata,
1362 + self::MAX_META_LENGTH,
1363 + self::MAX_SIZE_FULL_SYNC
1364 + );
1365 +
1366 + return array(
1367 + 'object_ids' => $filtered_order_item_ids,
1368 + 'objects' => $filtered_order_items,
1369 + 'meta' => $filtered_order_items_metadata,
1370 + );
1371 + }
1372 +}