PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
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 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-sync / src / modules / class-woocommerce.php

class-woocommerce.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at jetpack_vendor/automattic/jetpack-sync/src/modules/class-woocommerce.php

1,373 lines 47.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
1373