PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-sync/src/modules/class-woocommerce-hpos-orders.php +616 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,616 @@
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 +}