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
jetpack / jetpack_vendor / automattic / jetpack-sync / src / modules / class-woocommerce-hpos-orders.php

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

617 lines 17.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce HPOS orders sync module.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8 namespace Automattic\Jetpack\Sync\Modules;
9
10 use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit( 0 );
14 }
15
16 /**
17 * Adds WooCommerce HPOS specific data to sync when HPOS is enabled on the site.
18 */
19 class WooCommerce_HPOS_Orders extends Module {
20
21 /**
22 * The slug of WooCommerce Subscriptions plugin.
23 */
24 const WOOCOMMERCE_SUBSCRIPTIONS_PATH = 'woocommerce-subscriptions/woocommerce-subscriptions.php';
25
26 /**
27 * Order table name. There are four order tables (order, addresses, operational_data and meta), but for sync purposes we only care about the main table since it has the order ID.
28 *
29 * @access private
30 *
31 * @var string
32 */
33 private $order_table_name;
34
35 /**
36 * Sync module name.
37 *
38 * @access public
39 *
40 * @return string
41 */
42 public function name() {
43 return 'woocommerce_hpos_orders';
44 }
45
46 /**
47 * Get the order table name.
48 *
49 * @access public
50 *
51 * @return string
52 * @deprecated since 3.11.0 Use table() instead.
53 */
54 public function table_name() {
55 _deprecated_function( __METHOD__, '3.11.0', 'Automattic\\Jetpack\\Sync\\WooCommerce_HPOS_Orders->table' );
56 return $this->order_table_name;
57 }
58
59 /**
60 * The table in the database with the prefix.
61 *
62 * @access public
63 *
64 * @return string|bool
65 */
66 public function table() {
67 return $this->order_table_name;
68 }
69
70 /**
71 * Initialize order table data store, returns if the class don't exist (pre WC 6.x)
72 *
73 * @access public
74 */
75 public function __construct() {
76 if ( ! class_exists( OrdersTableDataStore::class ) ) {
77 return;
78 }
79 $this->order_table_name = OrdersTableDataStore::get_orders_table_name();
80 }
81
82 /**
83 * Get order types that we want to sync. Adding a new type here is not enough, we would also need to add its prop in filter_order_data method.
84 *
85 * @param bool $prefixed Whether to return prefixed types with shop_ or not.
86 *
87 * @return array Order types to sync.
88 */
89 public static function get_order_types_to_sync( $prefixed = false ) {
90 $types = array( 'order', 'order_refund' );
91
92 // Ensure this is available.
93 if ( ! function_exists( 'is_plugin_active' ) ) {
94 require_once ABSPATH . 'wp-admin/includes/plugin.php';
95 }
96
97 if ( is_plugin_active( self::WOOCOMMERCE_SUBSCRIPTIONS_PATH ) ) {
98 $types[] = 'subscription';
99 }
100
101 if ( $prefixed ) {
102 $types = array_map(
103 function ( $type ) {
104 return "shop_{$type}";
105 },
106 $types
107 );
108 }
109 return $types;
110 }
111
112 /**
113 * Hooks sync listners on order modify events.
114 *
115 * @access public
116 *
117 * @param callable $callable Action handler callable.
118 */
119 public function init_listeners( $callable ) {
120 foreach ( self::get_order_types_to_sync() as $type ) {
121 add_action( "woocommerce_after_{$type}_object_save", $callable );
122 add_filter( "jetpack_sync_before_enqueue_woocommerce_after_{$type}_object_save", array( $this, 'on_before_enqueue_order_save' ) );
123 }
124 add_action( 'woocommerce_delete_order', $callable );
125 add_action( 'woocommerce_delete_subscription', $callable );
126 add_filter( 'jetpack_sync_before_enqueue_woocommerce_delete_order', array( $this, 'on_before_enqueue_order_trash_delete' ) );
127 add_filter( 'jetpack_sync_before_enqueue_woocommerce_delete_subscription', array( $this, 'on_before_enqueue_order_trash_delete' ) );
128 add_action( 'woocommerce_trash_order', $callable );
129 add_action( 'woocommerce_trash_subscription', $callable );
130 add_filter( 'jetpack_sync_before_enqueue_woocommerce_trash_order', array( $this, 'on_before_enqueue_order_trash_delete' ) );
131 add_filter( 'jetpack_sync_before_enqueue_woocommerce_trash_subscription', array( $this, 'on_before_enqueue_order_trash_delete' ) );
132 }
133
134 /**
135 * Hooks the full sync listeners.
136 *
137 * @access public
138 *
139 * @param callable $callable Action handler callable.
140 */
141 public function init_full_sync_listeners( $callable ) {
142 add_action( 'jetpack_full_sync_orders', $callable );
143 }
144
145 /**
146 * Initialize the module in the sender.
147 *
148 * @access public
149 */
150 public function init_before_send() {
151 // Incremental Sync
152 foreach ( self::get_order_types_to_sync() as $type ) {
153 add_filter( "jetpack_sync_before_send_woocommerce_after_{$type}_object_save", array( $this, 'expand_order_object' ) );
154 }
155 // Full sync.
156 add_filter( 'jetpack_sync_before_send_jetpack_full_sync_woocommerce_hpos_orders', array( $this, 'build_full_sync_action_array' ) );
157 }
158
159 /**
160 * Returns the ID field from wc_orders table.
161 *
162 * @access public
163 *
164 * @return string
165 */
166 public function id_field() {
167 return 'id';
168 }
169
170 /**
171 * Retrieve the actions that will be sent for this module during a full sync.
172 *
173 * @access public
174 *
175 * @return array Full sync actions of this module.
176 */
177 public function get_full_sync_actions() {
178 return array( 'jetpack_full_sync_orders' );
179 }
180
181 /**
182 * Retrieve order data by its ID.
183 *
184 * @access public
185 *
186 * @param string $object_type Type of object to retrieve. Should be `order`.
187 * @param int $id Order ID.
188 *
189 * @return array
190 */
191 public function get_object_by_id( $object_type, $id ) {
192 if ( 'order' !== $object_type ) {
193 return $id;
194 }
195 $order_objects = $this->get_objects_by_id( $object_type, array( $id ) );
196 return $order_objects[ $id ] ?? false;
197 }
198
199 /**
200 * Retrieves multiple orders data by their ID. Sorted by ID in descending order.
201 *
202 * @access public
203 *
204 * @param string $object_type Type of object to retrieve. Should be `order`.
205 * @param array $ids List of order IDs.
206 *
207 * @return array
208 */
209 public function get_objects_by_id( $object_type, $ids ) {
210 if ( 'order' !== $object_type || empty( $ids ) || ! is_array( $ids ) ) {
211 return array();
212 }
213
214 $orders = wc_get_orders(
215 array(
216 'post__in' => $ids,
217 'type' => self::get_order_types_to_sync( true ),
218 'post_status' => self::get_all_possible_order_status_keys(),
219 'limit' => -1,
220 'orderby' => 'ID',
221 'order' => 'DESC',
222 )
223 );
224
225 $orders_data = array();
226 foreach ( $orders as $order ) {
227 $orders_data[ $order->get_id() ] = $this->filter_order_data( $order );
228 }
229
230 return $orders_data;
231 }
232
233 /**
234 * Retrieves multiple orders data by their ID.
235 *
236 * @access public
237 *
238 * @param array $args List of order IDs.
239 *
240 * @return array
241 * @deprecated since 4.7.0
242 */
243 public function expand_order_objects( $args ) {
244 _deprecated_function( __METHOD__, '4.7.0' );
245 list( $order_ids, $previous_end ) = $args;
246 return array(
247 'orders' => $this->get_objects_by_id( 'order', $order_ids ),
248 'previous_end' => $previous_end,
249 );
250 }
251
252 /**
253 * Build the full sync action object.
254 *
255 * @access public
256 *
257 * @param array $args An array with filtered objects and previous end.
258 *
259 * @return array An array with orders and previous end.
260 */
261 public function build_full_sync_action_array( $args ) {
262 list( $filtered_orders, $previous_end ) = $args;
263 return array(
264 'orders' => $filtered_orders['objects'],
265 'previous_end' => $previous_end,
266 );
267 }
268
269 /**
270 * Retrieve filtered order data before sending.
271 *
272 * @access public
273 *
274 * @param array $args An array with order data.
275 *
276 * @return array|false
277 */
278 public function expand_order_object( $args ) {
279 if ( empty( $args['id'] ) ) {
280 return false;
281 }
282
283 $order_object = wc_get_order( $args['id'] );
284
285 if ( ! $order_object instanceof \WC_Abstract_Order ) {
286 return false;
287 }
288
289 return $this->filter_order_data( $order_object );
290 }
291
292 /**
293 * Retrieve order data by its ID.
294 *
295 * @access public
296 *
297 * @param array $args Order ID.
298 *
299 * @return array|false
300 */
301 public function on_before_enqueue_order_save( $args ) {
302 // Prevent multiple triggers on a single request.
303 static $processed = array();
304
305 if ( ! is_array( $args ) || ! isset( $args[0] ) ) {
306 return false;
307 }
308 $order_object = $args[0];
309
310 if ( is_int( $order_object ) ) {
311 $order_object = wc_get_order( $order_object );
312 }
313
314 if ( ! $order_object instanceof \WC_Abstract_Order ) {
315 return false;
316 }
317
318 $order_id = $order_object->get_id();
319
320 if ( empty( $order_id ) ) {
321 return false;
322 }
323
324 if ( isset( $processed[ $order_id ] ) ) {
325 return false;
326 }
327
328 $processed[ $order_id ] = true;
329
330 return array( 'id' => $order_id );
331 }
332
333 /**
334 * Convert order ID to array.
335 *
336 * @access public
337 *
338 * @param array $args Order ID.
339 *
340 * @return array
341 */
342 public function on_before_enqueue_order_trash_delete( $args ) {
343 if ( ! is_array( $args ) || ! isset( $args[0] ) ) {
344 return false;
345 }
346 $order_id = $args[0];
347
348 if ( ! is_int( $order_id ) ) {
349 return false;
350 }
351
352 return array( 'id' => $order_id );
353 }
354
355 /**
356 * Filters only allowed keys from order data. No PII etc information is allowed to be synced.
357 *
358 * @access private
359 *
360 * @param \WC_Abstract_Order $order_object Order object.
361 *
362 * @return array Filtered order data.
363 */
364 private function filter_order_data( $order_object ) {
365 // Filter with allowlist.
366 $allowed_data_keys = WooCommerce::$wc_post_meta_whitelist;
367 $core_table_keys = array(
368 'id',
369 'status',
370 'date_created',
371 'date_modified',
372 'parent_id',
373 );
374 $allowed_data_keys = array_merge( $allowed_data_keys, $core_table_keys );
375 $filtered_order_data = array( 'type' => $order_object->get_type() );
376 $order_data = $order_object->get_data();
377 foreach ( $allowed_data_keys as $key ) {
378 $key = trim( $key, '_' );
379 $key_parts = explode( '_', $key );
380
381 if ( in_array( $key_parts[0], array( 'order', 'refund' ), true ) ) {
382 if ( isset( $order_data[ $key_parts[1] ] ) && ! is_array( $order_data[ $key_parts[1] ] ) ) {
383 $filtered_order_data[ $key ] = $order_data[ $key_parts[1] ];
384 continue;
385 }
386 }
387
388 if ( in_array( $key_parts[0], array( 'billing', 'shipping' ), true ) && 2 === count( $key_parts ) ) {
389 if ( isset( $order_data[ $key_parts[0] ][ $key_parts[1] ] ) ) {
390 $filtered_order_data[ $key ] = $order_data[ $key_parts[0] ][ $key_parts[1] ];
391 continue;
392 }
393 }
394
395 /**
396 * We need to convert the WC_DateTime objects to stdClass objects to ensure they are properly encoded.
397 *
398 * @see Automattic\Jetpack\Sync\Functions::json_wrap as the return value of get_object_vars can vary depending on PHP version.
399 */
400 if ( in_array( $key, array( 'date_created', 'date_modified', 'date_paid', 'date_completed' ), true ) && isset( $order_data[ $key ] ) ) {
401 if ( is_a( $order_data[ $key ], 'WC_DateTime' ) ) {
402 $filtered_order_data[ $key ] = (object) (array) $order_data[ $key ];
403 continue;
404 }
405 }
406
407 if ( isset( $order_data[ $key ] ) ) {
408 $filtered_order_data[ $key ] = $order_data[ $key ];
409 continue;
410 }
411
412 switch ( $key ) {
413 case 'cart_discount':
414 $filtered_order_data[ $key ] = $order_data['discount_total'] ?? '';
415 break;
416 case 'cart_discount_tax':
417 $filtered_order_data[ $key ] = $order_data['discount_tax'] ?? '';
418 break;
419 case 'order_shipping':
420 $filtered_order_data[ $key ] = $order_data['shipping_total'] ?? '';
421 break;
422 case 'order_shipping_tax':
423 $filtered_order_data[ $key ] = $order_data['shipping_tax'] ?? '';
424 break;
425 case 'order_tax':
426 $filtered_order_data[ $key ] = $order_data['cart_tax'] ?? '';
427 break;
428 case 'order_total':
429 $filtered_order_data[ $key ] = $order_data['total'] ?? '';
430 break;
431 }
432 }
433 if ( '' === $filtered_order_data['status'] ) {
434 $filtered_order_data['status'] = 'pending';
435 }
436 $filtered_order_data['status'] = self::get_wc_order_status_with_prefix( $filtered_order_data['status'] );
437
438 /**
439 * Filter the order data before syncing.
440 *
441 * @since 3.7.0
442 *
443 * @param array $filtered_order_data The Filtered order data.
444 * @param \WC_Abstract_Order $order_object The Order object.
445 */
446 return apply_filters( 'jetpack_sync_filtered_hpos_order_data', $filtered_order_data, $order_object );
447 }
448
449 /**
450 * Returns all possible order status keys, including 'auto-draft' and 'trash'.
451 *
452 * @access public
453 *
454 * @return array An array of all possible status keys, including 'auto-draft' and 'trash'.
455 */
456 public static function get_all_possible_order_status_keys() {
457 $order_statuses = array( 'auto-draft', 'trash' );
458 $wc_order_statuses = self::wc_get_order_status_keys();
459
460 return array_unique( array_merge( $wc_order_statuses, $order_statuses ) );
461 }
462
463 /**
464 * Add the 'wc-' order status to WC related order statuses.
465 *
466 * @param string $status The WC order status without the 'wc-' prefix.
467 *
468 * @return string The WC order status with the 'wc-' prefix if it's a valid order status, initial $status otherwise.
469 */
470 public static function get_wc_order_status_with_prefix( string $status ) {
471 return in_array( 'wc-' . $status, self::wc_get_order_status_keys(), true ) ? 'wc-' . $status : $status;
472 }
473
474 /**
475 * Returns order status keys using 'wc_get_order_statuses', if possible.
476 *
477 * @see wc_get_order_statuses
478 *
479 * @return array Filtered order metadata.
480 */
481 private static function wc_get_order_status_keys() {
482 $wc_order_statuses = array();
483 if ( function_exists( 'wc_get_order_statuses' ) ) {
484 $wc_order_statuses = array_keys( wc_get_order_statuses() );
485 $wc_order_statuses[] = 'wc-checkout-draft'; // Temp till Woo fixes a bug where this order status is missing.
486 } else {
487 $wc_order_statuses = array(
488 'wc-pending',
489 'wc-processing',
490 'wc-on-hold',
491 'wc-completed',
492 'wc-cancelled',
493 'wc-refunded',
494 'wc-failed',
495 'wc-checkout-draft',
496 );
497 }
498
499 if ( function_exists( 'wcs_get_subscription_statuses' ) ) {
500 // @phan-suppress-next-line PhanUndeclaredFunction -- Checked above. See also https://github.com/phan/phan/issues/1204.
501 $wc_subscription_statuses = array_keys( wcs_get_subscription_statuses() );
502 $wc_order_statuses = array_merge( $wc_order_statuses, $wc_subscription_statuses );
503 }
504
505 return array_unique( $wc_order_statuses );
506 }
507
508 /**
509 * Returns metadata for order object.
510 *
511 * @access protected
512 *
513 * @param array $ids List of order IDs.
514 * @param string $meta_type Meta type.
515 * @param array $meta_key_whitelist List of allowed meta keys.
516 *
517 * @return array Filtered order metadata.
518 */
519 protected function get_metadata( $ids, $meta_type, $meta_key_whitelist ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- returning empty meta is intentional.
520 return array(); // don't sync metadata, all allow-listed core data is available in the order object.
521 }
522
523 /**
524 * Retrieve an estimated number of actions that will be enqueued.
525 *
526 * @access public
527 *
528 * @param array $config Full sync configuration for this sync module.
529 * @return int Number of items yet to be enqueued.
530 */
531 public function estimate_full_sync_actions( $config ) {
532 global $wpdb;
533
534 $query = "SELECT count(*) FROM {$this->table()} WHERE {$this->get_where_sql( $config ) }";
535 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Hardcoded query, no user variable
536 $count = (int) $wpdb->get_var( $query );
537
538 return (int) ceil( $count / self::ARRAY_CHUNK_SIZE );
539 }
540
541 /**
542 * Enqueue the WooCommerce HPOS orders actions for full sync.
543 *
544 * @access public
545 *
546 * @param array $config Full sync configuration for this sync module.
547 * @param int $max_items_to_enqueue Maximum number of items to enqueue.
548 * @param boolean $state True if full sync has finished enqueueing this module, false otherwise.
549 * @return array Number of actions enqueued, and next module state.
550 */
551 public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) {
552 return $this->enqueue_all_ids_as_action( 'full_sync_orders', $this->table(), 'id', $this->get_where_sql( $config ), $max_items_to_enqueue, $state );
553 }
554
555 /**
556 * Get where SQL for full sync.
557 *
558 * @access public
559 *
560 * @param array $config Full sync configuration for this sync module.
561 *
562 * @return string WHERE SQL clause, or `null` if no comments are specified in the module config.
563 */
564 public function get_where_sql( $config ) {
565 global $wpdb;
566 $parent_where = parent::get_where_sql( $config );
567 $order_types = self::get_order_types_to_sync( true );
568 $order_type_placeholder = implode( ', ', array_fill( 0, count( $order_types ), '%s' ) );
569 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Query is prepared.
570 $where_sql = $wpdb->prepare( "type IN ( $order_type_placeholder )", $order_types );
571 return "{$parent_where} AND {$where_sql}";
572 }
573
574 /**
575 * Given the Module Configuration and Status return the next chunk of items to send.
576 * This function also expands the posts and metadata and filters them based on the maximum size constraints.
577 *
578 * @param array $config This module Full Sync configuration.
579 * @param array $status This module Full Sync status.
580 * @param int $chunk_size Chunk size.
581 *
582 * @return array
583 */
584 public function get_next_chunk( $config, $status, $chunk_size ) {
585
586 $order_ids = parent::get_next_chunk( $config, $status, $chunk_size );
587
588 if ( empty( $order_ids ) ) {
589 return array();
590 }
591
592 $orders = $this->get_objects_by_id( 'order', $order_ids );
593
594 // If no orders were fetched, make sure to return the expected structure so that status is updated correctly.
595 if ( empty( $orders ) ) {
596 return array(
597 'object_ids' => $order_ids,
598 'objects' => array(),
599 );
600 }
601
602 // Filter the orders based on the maximum size constraints. We don't need to filter metadata here since we don't sync it for hpos.
603 list( $filtered_order_ids, $filtered_orders, ) = $this->filter_objects_and_metadata_by_size(
604 'order',
605 $orders,
606 array(),
607 0,
608 self::MAX_SIZE_FULL_SYNC
609 );
610
611 return array(
612 'object_ids' => $filtered_order_ids,
613 'objects' => $filtered_orders,
614 );
615 }
616 }
617