PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
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 13.7.2 All 507 releases
← All changes | jetpack_vendor/automattic/jetpack-sync/src/modules/class-woocommerce.php +752 -19 13.7.2 → 16.3-beta View file →
@@ -9,8 +9,12 @@
9 9
10 10 use WC_Order;
11 11 use WP_Error;
12 12
13 +if ( ! defined( 'ABSPATH' ) ) {
14 + exit( 0 );
15 +}
16 +
13 17 /**
14 18 * Class to handle sync for WooCommerce.
15 19 */
16 20 class WooCommerce extends Module {
@@ -51,8 +55,40 @@
51 55 'discount_amount_tax',
52 56 );
53 57
54 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 + /**
55 91 * Name of the order item database table.
56 92 *
57 93 * @access private
58 94 *
@@ -60,19 +96,85 @@
60 96 */
61 97 private $order_item_table_name;
62 98
63 99 /**
64 - * The table in the database.
100 + * Customer detail meta changes to sync at the end of the request.
65 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 + *
66 132 * @access public
67 133 *
68 134 * @return string
135 + * @deprecated since 3.11.0 Use table() instead.
69 136 */
70 137 public function table_name() {
138 + _deprecated_function( __METHOD__, '3.11.0', 'Automattic\\Jetpack\\Sync\\WooCommerce->table' );
71 139 return $this->order_item_table_name;
72 140 }
73 141
74 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 + /**
75 177 * Constructor.
76 178 *
77 179 * @global $wpdb
78 180 *
@@ -88,9 +190,13 @@
88 190 add_filter( 'jetpack_sync_post_meta_whitelist', array( $this, 'add_woocommerce_post_meta_whitelist' ), 10 );
89 191 add_filter( 'jetpack_sync_comment_meta_whitelist', array( $this, 'add_woocommerce_comment_meta_whitelist' ), 10 );
90 192
91 193 add_filter( 'jetpack_sync_before_enqueue_woocommerce_new_order_item', array( $this, 'filter_order_item' ) );
92 - add_filter( 'jetpack_sync_before_enqueue_woocommerce_update_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' ) );
93 199 add_filter( 'jetpack_sync_whitelisted_comment_types', array( $this, 'add_review_comment_types' ) );
94 200
95 201 // Blacklist Action Scheduler comment types.
96 202 add_filter( 'jetpack_sync_prevent_sending_comment_data', array( $this, 'filter_action_scheduler_comments' ), 10, 2 );
@@ -122,19 +228,24 @@
122 228 add_action( 'woocommerce_attribute_added', $callable, 10, 2 );
123 229 add_action( 'woocommerce_attribute_updated', $callable, 10, 3 );
124 230 add_action( 'woocommerce_attribute_deleted', $callable, 10, 3 );
125 231
126 - // Orders.
127 - add_action( 'woocommerce_new_order', $callable, 10, 1 );
128 - add_action( 'woocommerce_order_status_changed', $callable, 10, 3 );
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 );
129 240 add_action( 'woocommerce_payment_complete', $callable, 10, 1 );
130 241
131 242 // Order items.
132 243 add_action( 'woocommerce_new_order_item', $callable, 10, 4 );
133 - add_action( 'woocommerce_update_order_item', $callable, 10, 4 );
134 244 add_action( 'woocommerce_delete_order_item', $callable, 10, 1 );
135 245 add_action( 'woocommerce_remove_order_item_ids', $callable, 10, 1 );
136 246 $this->init_listeners_for_meta_type( 'order_item', $callable );
247 + $this->init_meta_whitelist_handler( 'order_item', array( $this, 'filter_meta' ) );
137 248
138 249 // Payment tokens.
139 250 add_action( 'woocommerce_new_payment_token', $callable, 10, 1 );
140 251 add_action( 'woocommerce_payment_token_deleted', $callable, 10, 2 );
@@ -145,8 +256,9 @@
145 256 add_action( 'woocommerce_downloadable_product_download_log_insert', $callable, 10, 1 );
146 257 add_action( 'woocommerce_grant_product_download_access', $callable, 10, 1 );
147 258
148 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.
149 261 add_action( 'woocommerce_tax_rate_added', $callable, 10, 2 );
150 262 add_action( 'woocommerce_tax_rate_updated', $callable, 10, 2 );
151 263 add_action( 'woocommerce_tax_rate_deleted', $callable, 10, 1 );
152 264
@@ -153,8 +265,17 @@
153 265 // Webhooks.
154 266 add_action( 'woocommerce_new_webhook', $callable, 10, 1 );
155 267 add_action( 'woocommerce_webhook_deleted', $callable, 10, 2 );
156 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 );
157 278 }
158 279
159 280 /**
160 281 * Initialize WooCommerce action listeners for full sync.
@@ -184,9 +305,9 @@
184 305 * @access public
185 306 */
186 307 public function init_before_send() {
187 308 // Full sync.
188 - add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_order_items', array( $this, 'expand_order_item_ids' ) );
309 + add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_order_items', array( $this, 'build_full_sync_action_array' ) );
189 310 }
190 311
191 312 /**
192 313 * Expand the order items properly.
@@ -202,8 +323,372 @@
202 323 return $args;
203 324 }
204 325
205 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 + /**
206 691 * Retrieve the order item ids to be removed and send them as one action
207 692 *
208 693 * @param WC_Order $order The order argument.
209 694 * @param string $type Order item type.
@@ -229,10 +714,12 @@
229 714 * @todo Refactor table name to use a $wpdb->prepare placeholder.
230 715 *
231 716 * @param array $args The hook arguments.
232 717 * @return array $args Expanded order items with meta.
718 + * @deprecated since 4.7.0
233 719 */
234 720 public function expand_order_item_ids( $args ) {
721 + _deprecated_function( __METHOD__, '4.7.0' );
235 722 $order_item_ids = $args[0];
236 723
237 724 global $wpdb;
238 725
@@ -237,8 +724,9 @@
237 724 global $wpdb;
238 725
239 726 $order_item_ids_sql = implode( ', ', array_map( 'intval', $order_item_ids ) );
240 727
728 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
241 729 $order_items = $wpdb->get_results(
242 730 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
243 731 "SELECT * FROM $this->order_item_table_name WHERE order_item_id IN ( $order_item_ids_sql )"
244 732 );
@@ -247,23 +735,20 @@
247 735 $order_items,
248 736 $this->get_metadata( $order_item_ids, 'order_item', static::$order_item_meta_whitelist ),
249 737 );
250 738 }
251 -
252 739 /**
253 740 * Extract the full order item from the database by its ID.
254 741 *
255 742 * @access public
256 743 *
257 - * @todo Refactor table name to use a $wpdb->prepare placeholder.
258 - *
259 744 * @param int $order_item_id Order item ID.
260 745 * @return object Order item.
261 746 */
262 747 public function build_order_item( $order_item_id ) {
263 748 global $wpdb;
264 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
265 - return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $this->order_item_table_name WHERE order_item_id = %d", $order_item_id ) );
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 ) );
266 751 }
267 752
268 753 /**
269 754 * Enqueue the WooCommerce actions for full sync.
@@ -286,9 +771,9 @@
286 771 *
287 772 * @todo Refactor the SQL query to use $wpdb->prepare().
288 773 *
289 774 * @param array $config Full sync configuration for this sync module.
290 - * @return array Number of items yet to be enqueued.
775 + * @return int Number of items yet to be enqueued.
291 776 */
292 777 public function estimate_full_sync_actions( $config ) {
293 778 global $wpdb;
294 779
@@ -317,9 +802,9 @@
317 802 * @param array $list Existing options whitelist.
318 803 * @return array Updated options whitelist.
319 804 */
320 805 public function add_woocommerce_options_whitelist( $list ) {
321 - return array_merge( $list, self::$wc_options_whitelist );
806 + return array_values( array_unique( array_merge( $list, self::$wc_options_whitelist ) ) );
322 807 }
323 808
324 809 /**
325 810 * Add WooCommerce constants to the constants whitelist.
@@ -428,10 +913,188 @@
428 913 'woocommerce_currency_pos',
429 914 'woocommerce_api_enabled',
430 915 'woocommerce_allow_tracking',
431 916 'woocommerce_task_list_hidden',
432 - 'woocommerce_onboarding_profile',
433 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',
434 1097 );
435 1098
436 1099 /**
437 1100 * Whitelist for constants we are interested to sync.
@@ -548,10 +1211,10 @@
548 1211 '_prices_include_tax',
549 1212 '_date_completed',
550 1213 '_date_paid',
551 1214 '_payment_tokens',
552 - '_billing_address_index',
553 - '_shipping_address_index',
1215 + // '_billing_address_index', do not sync these as they contain personal data.
1216 + // '_shipping_address_index',
554 1217 '_recorded_sales',
555 1218 '_recorded_coupon_usage_counts',
556 1219 // See https://github.com/woocommerce/woocommerce/blob/8ed6e7436ff87c2153ed30edd83c1ab8abbdd3e9/includes/data-stores/class-wc-order-data-store-cpt.php#L539 .
557 1220 '_download_permissions_granted',
@@ -607,15 +1270,16 @@
607 1270
608 1271 /**
609 1272 * Returns a list of order_item objects by their IDs.
610 1273 *
611 - * @param array $ids List of order_item IDs to fetch.
1274 + * @param array $ids List of order_item IDs to fetch.
1275 + * @param string $order Either 'ASC' or 'DESC'.
612 1276 *
613 1277 * @access public
614 1278 *
615 1279 * @return array|object|null
616 1280 */
617 - public function get_order_item_by_ids( $ids ) {
1281 + public function get_order_item_by_ids( $ids, $order = '' ) {
618 1282 global $wpdb;
619 1283
620 1284 if ( ! is_array( $ids ) ) {
621 1285 return array();
@@ -631,9 +1295,78 @@
631 1295 // Prepare the placeholders for the prepared query below.
632 1296 $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
633 1297
634 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 + }
635 1302
636 1303 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
637 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 + );
638 1371 }
639 1372 }