PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Feature / WooCommerce / Orders.php

Orders.php in ElasticPress 5.3.5, at includes/classes/Feature/WooCommerce/Orders.php

530 lines 15.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce Orders
4 *
5 * @since 4.7.0
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Feature\WooCommerce;
10
11 use ElasticPress\Indexables;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit; // Exit if accessed directly.
15 }
16
17 /**
18 * WooCommerce Orders
19 */
20 class Orders {
21 /**
22 * WooCommerce feature object instance
23 *
24 * @var WooCommerce
25 */
26 protected $woocommerce;
27
28 /**
29 * Class constructor
30 *
31 * @param WooCommerce $woocommerce WooCommerce feature object instance
32 */
33 public function __construct( WooCommerce $woocommerce ) {
34 $this->woocommerce = $woocommerce;
35 }
36
37 /**
38 * Setup order related hooks
39 */
40 public function setup() {
41 add_filter( 'ep_sync_insert_permissions_bypass', [ $this, 'bypass_order_permissions_check' ], 10, 2 );
42 add_filter( 'ep_prepare_meta_allowed_protected_keys', [ $this, 'allow_meta_keys' ], 10, 2 );
43 add_filter( 'ep_post_sync_args_post_prepare_meta', [ $this, 'add_order_items_search' ], 20, 2 );
44 add_filter( 'ep_pc_skip_post_content_cleanup', [ $this, 'keep_order_fields' ], 20, 2 );
45 add_action( 'parse_query', [ $this, 'maybe_hook_woocommerce_search_fields' ], 1 );
46 add_action( 'parse_query', [ $this, 'search_order' ], 11 );
47 add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 );
48 add_filter( 'ep_admin_notices', [ $this, 'hpos_compatibility_notice' ] );
49 }
50
51 /**
52 * Unsetup order related hooks
53 *
54 * @since 5.0.0
55 */
56 public function tear_down() {
57 remove_filter( 'ep_sync_insert_permissions_bypass', [ $this, 'bypass_order_permissions_check' ] );
58 remove_filter( 'ep_prepare_meta_allowed_protected_keys', [ $this, 'allow_meta_keys' ] );
59 remove_filter( 'ep_post_sync_args_post_prepare_meta', [ $this, 'add_order_items_search' ], 20 );
60 remove_filter( 'ep_pc_skip_post_content_cleanup', [ $this, 'keep_order_fields' ], 20 );
61 remove_action( 'parse_query', [ $this, 'maybe_hook_woocommerce_search_fields' ], 1 );
62 remove_action( 'parse_query', [ $this, 'search_order' ], 11 );
63 remove_action( 'pre_get_posts', [ $this, 'translate_args' ], 11 );
64 }
65
66 /**
67 * Allow order creations on the front end to get synced
68 *
69 * @param bool $override Original order perms check value
70 * @param int $post_id Post ID
71 * @return bool
72 */
73 public function bypass_order_permissions_check( $override, $post_id ) {
74 $searchable_post_types = $this->get_admin_searchable_post_types();
75
76 if ( in_array( get_post_type( $post_id ), $searchable_post_types, true ) ) {
77 return true;
78 }
79
80 return $override;
81 }
82
83 /**
84 * Returns the WooCommerce-oriented post types in admin that EP will search
85 *
86 * @return array
87 */
88 public function get_admin_searchable_post_types() {
89 $searchable_post_types = array( 'shop_order' );
90
91 /**
92 * Filter admin searchable WooCommerce post types
93 *
94 * @hook ep_woocommerce_admin_searchable_post_types
95 * @since 4.4.0
96 * @param {array} $post_types Post types
97 * @return {array} New post types
98 */
99 return apply_filters( 'ep_woocommerce_admin_searchable_post_types', $searchable_post_types );
100 }
101
102 /**
103 * Index WooCommerce orders meta fields
104 *
105 * @param array $meta Existing post meta
106 * @param \WP_Post $post Post object.
107 * @return array
108 */
109 public function allow_meta_keys( $meta, $post ) {
110 if ( ! in_array( $post->post_type, $this->get_supported_post_types(), true ) ) {
111 return $meta;
112 }
113
114 return array_unique(
115 array_merge(
116 $meta,
117 array(
118 '_customer_user',
119 '_order_key',
120 '_billing_company',
121 '_billing_address_1',
122 '_billing_address_2',
123 '_billing_city',
124 '_billing_postcode',
125 '_billing_country',
126 '_billing_state',
127 '_billing_email',
128 '_billing_phone',
129 '_shipping_address_1',
130 '_shipping_address_2',
131 '_shipping_city',
132 '_shipping_postcode',
133 '_shipping_country',
134 '_shipping_state',
135 '_billing_last_name',
136 '_billing_first_name',
137 '_shipping_first_name',
138 '_shipping_last_name',
139 '_variations_skus',
140 )
141 )
142 );
143 }
144
145 /**
146 * Add order items as a searchable string.
147 *
148 * This mimics how WooCommerce currently does in the order_itemmeta
149 * table. They combine the titles of the products and put them in a
150 * meta field called "Items".
151 *
152 * @param array $post_args Post arguments
153 * @param string|int $post_id Post id
154 *
155 * @return array
156 */
157 public function add_order_items_search( $post_args, $post_id ) {
158 $order = wc_get_order( $post_id );
159 if ( ! $order ) {
160 return $post_args;
161 }
162
163 $searchable_post_types = $this->get_admin_searchable_post_types();
164
165 // Make sure it is only WooCommerce orders we touch.
166 if ( ! in_array( $post_args['post_type'], $searchable_post_types, true ) ) {
167 return $post_args;
168 }
169
170 $post_indexable = Indexables::factory()->get( 'post' );
171
172 // Get order items.
173 $item_meta = [];
174 foreach ( $order->get_items() as $delta => $product_item ) {
175 // WooCommerce 3.x uses WC_Order_Item_Product instance while 2.x an array
176 if ( is_object( $product_item ) && method_exists( $product_item, 'get_name' ) ) {
177 $item_meta['_items'][] = $product_item->get_name( 'edit' );
178 } elseif ( is_array( $product_item ) && isset( $product_item['name'] ) ) {
179 $item_meta['_items'][] = $product_item['name'];
180 }
181 }
182
183 // Prepare order items.
184 $item_meta['_items'] = empty( $item_meta['_items'] ) ? '' : implode( '|', $item_meta['_items'] );
185 $post_args['meta'] = array_merge( $post_args['meta'], $post_indexable->prepare_meta_types( $item_meta ) );
186
187 return $post_args;
188 }
189
190 /**
191 * Prevent order fields from being removed.
192 *
193 * When Protected Content is enabled, all posts with password have their content removed.
194 * This can't happen for orders, as the order key is added in that field.
195 *
196 * @see https://github.com/10up/ElasticPress/issues/2726
197 *
198 * @param bool $skip Whether the password protected content should have their content, and meta removed
199 * @param array $post_args Post arguments
200 * @return bool
201 */
202 public function keep_order_fields( $skip, $post_args ) {
203 $searchable_post_types = $this->get_admin_searchable_post_types();
204
205 if ( in_array( $post_args['post_type'], $searchable_post_types, true ) ) {
206 return true;
207 }
208
209 return $skip;
210 }
211
212 /**
213 * Sets WooCommerce meta search fields to an empty array if we are integrating the main query with ElasticSearch
214 *
215 * WooCommerce calls this action as part of its own callback on parse_query. We add this filter only if the query
216 * is integrated with ElasticSearch.
217 * If we were to always return array() on this filter, we'd break admin searches when WooCommerce module is activated
218 * without the Protected Content Module
219 *
220 * @param \WP_Query $query Current query
221 */
222 public function maybe_hook_woocommerce_search_fields( $query ) {
223 global $pagenow, $wp, $wc_list_table;
224
225 if ( ! $this->woocommerce->should_integrate_with_query( $query ) ) {
226 return;
227 }
228
229 /**
230 * Determines actions to be applied, or removed, if doing a WooCommerce serarch
231 *
232 * @hook ep_woocommerce_hook_search_fields
233 * @since 4.4.0
234 */
235 do_action( 'ep_woocommerce_hook_search_fields' );
236
237 if ( 'edit.php' !== $pagenow || empty( $wp->query_vars['s'] ) || 'shop_order' !== $wp->query_vars['post_type'] || ! isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
238 return;
239 }
240
241 remove_action( 'parse_query', [ $wc_list_table, 'search_custom_fields' ] );
242 }
243
244 /**
245 * Enhance WooCommerce search order by order id, email, phone number, name, etc..
246 * What this function does:
247 * 1. Reverse the woocommerce shop_order_search_custom_fields query
248 * 2. If the search key is integer and it is an Order Id, just query with post__in
249 * 3. If the search key is integer but not an order id ( might be phone number ), use ES to find it
250 *
251 * @param WP_Query $wp WP Query
252 */
253 public function search_order( $wp ) {
254 global $pagenow;
255
256 if ( ! $this->woocommerce->should_integrate_with_query( $wp ) ) {
257 return;
258 }
259
260 $searchable_post_types = $this->get_admin_searchable_post_types();
261
262 if ( 'edit.php' !== $pagenow || empty( $wp->query_vars['post_type'] ) || ! in_array( $wp->query_vars['post_type'], $searchable_post_types, true ) ||
263 ( empty( $wp->query_vars['s'] ) && empty( $wp->query_vars['shop_order_search'] ) ) ) {
264 return;
265 }
266
267 // phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
268 if ( isset( $_GET['s'] ) ) {
269 $search_key_safe = str_replace( array( 'Order #', '#' ), '', wc_clean( $_GET['s'] ) );
270 unset( $wp->query_vars['post__in'] );
271 $wp->query_vars['s'] = $search_key_safe;
272 }
273 // phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
274 }
275
276 /**
277 * Determines whether or not ES should be integrating with the provided query
278 *
279 * @param \WP_Query $query Query we might integrate with
280 * @return bool
281 */
282 public function should_integrate_with_query( \WP_Query $query ): bool {
283 /**
284 * Check the post type
285 */
286 $supported_post_types = $this->get_supported_post_types( $query );
287 $post_type = $query->get( 'post_type', false );
288 if ( ! empty( $post_type ) &&
289 ( in_array( $post_type, $supported_post_types, true ) ||
290 ( is_array( $post_type ) && ! array_diff( $post_type, $supported_post_types ) ) )
291 ) {
292 return true;
293 }
294
295 return false;
296 }
297
298 /**
299 * Get the supported post types for Order related queries
300 *
301 * @return array
302 */
303 public function get_supported_post_types(): array {
304 $post_types = [ 'shop_order', 'shop_order_refund' ];
305
306 /**
307 * DEPRECATED. Expands or contracts the post_types eligible for indexing.
308 *
309 * @hook ep_woocommerce_default_supported_post_types
310 * @since 4.4.0
311 * @param {array} $post_types Post types
312 * @return {array} New post types
313 */
314 $supported_post_types = apply_filters_deprecated(
315 'ep_woocommerce_default_supported_post_types',
316 [ $post_types ],
317 '4.7.0',
318 'ep_woocommerce_orders_supported_post_types'
319 );
320
321 /**
322 * Expands or contracts the post_types related to orders eligible for indexing.
323 *
324 * @hook ep_woocommerce_orders_supported_post_types
325 * @since 4.7.0
326 * @param {array} $supported_post_types Post types
327 * @return {array} New post types
328 */
329 $supported_post_types = apply_filters( 'ep_woocommerce_orders_supported_post_types', $supported_post_types );
330
331 $supported_post_types = array_intersect(
332 $supported_post_types,
333 Indexables::factory()->get( 'post' )->get_indexable_post_types()
334 );
335
336 return $supported_post_types;
337 }
338
339 /**
340 * Display a notice if WooCommerce Orders are not compatible with ElasticPress
341 *
342 * If the user has WooCommerce, Protected Content, and HPOS enabled, orders will not go through ElasticPress.
343 *
344 * @param array $notices Current EP notices
345 * @return array
346 */
347 public function hpos_compatibility_notice( array $notices ): array {
348 $current_screen = \get_current_screen();
349 if ( empty( $current_screen->id ) || 'woocommerce_page_wc-orders' !== $current_screen->id ) {
350 return $notices;
351 }
352
353 if ( \ElasticPress\Utils\get_option( 'ep_hide_wc_orders_incompatible_notice' ) ) {
354 return $notices;
355 }
356
357 $protected_content = \ElasticPress\Features::factory()->get_registered_feature( 'protected_content' );
358 if ( ! $protected_content->is_active() ) {
359 return $notices;
360 }
361
362 if (
363 ! class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
364 || ! method_exists( '\Automattic\WooCommerce\Utilities\OrderUtil', 'custom_orders_table_usage_is_enabled' ) ) {
365 return $notices;
366 }
367
368 if ( ! \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) {
369 return $notices;
370 }
371
372 $notices['wc_orders_incompatible'] = [
373 'html' => esc_html__( "Although the WooCommerce and Protected Content features are enabled, ElasticPress will not integrate with the WooCommerce Orders list if WooCommerce's High-performance order storage is enabled.", 'elasticpress' ),
374 'type' => 'warning',
375 'dismiss' => true,
376 'scope' => 'site',
377 ];
378
379 return $notices;
380 }
381
382 /**
383 * If the query has a search term, add the order fields that need to be searched.
384 *
385 * @param \WP_Query $query The WP_Query
386 * @return \WP_Query
387 */
388 protected function maybe_set_search_fields( \WP_Query $query ) {
389 $search_term = $this->woocommerce->get_search_term( $query );
390 if ( empty( $search_term ) ) {
391 return $query;
392 }
393
394 $searchable_post_types = $this->get_admin_searchable_post_types();
395
396 $post_type = $query->get( 'post_type', false );
397 if ( ! in_array( $post_type, $searchable_post_types, true ) ) {
398 return $query;
399 }
400
401 $default_search_fields = array( 'post_title', 'post_content', 'post_excerpt' );
402 if ( ctype_digit( $search_term ) ) {
403 $default_search_fields[] = 'ID';
404 }
405 $search_fields = $query->get( 'search_fields', $default_search_fields );
406
407 $search_fields['meta'] = array_map(
408 'wc_clean',
409 /**
410 * Filter shop order meta fields to search for WooCommerce
411 *
412 * @hook shop_order_search_fields
413 * @param {array} $fields Shop order fields
414 * @return {array} New fields
415 */
416 apply_filters(
417 'shop_order_search_fields',
418 array(
419 '_order_key',
420 '_billing_company',
421 '_billing_address_1',
422 '_billing_address_2',
423 '_billing_city',
424 '_billing_postcode',
425 '_billing_country',
426 '_billing_state',
427 '_billing_email',
428 '_billing_phone',
429 '_shipping_address_1',
430 '_shipping_address_2',
431 '_shipping_city',
432 '_shipping_postcode',
433 '_shipping_country',
434 '_shipping_state',
435 '_billing_last_name',
436 '_billing_first_name',
437 '_shipping_first_name',
438 '_shipping_last_name',
439 '_items',
440 )
441 )
442 );
443
444 $query->set(
445 'search_fields',
446 /**
447 * Filter all the shop order fields to search for WooCommerce
448 *
449 * @hook ep_woocommerce_shop_order_search_fields
450 * @since 4.0.0
451 * @param {array} $fields Shop order fields
452 * @param {WP_Query} $query WP Query
453 * @return {array} New fields
454 */
455 apply_filters( 'ep_woocommerce_shop_order_search_fields', $search_fields, $query )
456 );
457 }
458
459 /**
460 * Translate args to ElasticPress compat format. This is the meat of what the feature does
461 *
462 * @param \WP_Query $query WP Query
463 */
464 public function translate_args( $query ) {
465 if ( ! $this->woocommerce->should_integrate_with_query( $query ) ) {
466 return;
467 }
468
469 if ( ! $this->should_integrate_with_query( $query ) ) {
470 return;
471 }
472
473 $query->set( 'ep_integrate', true );
474
475 /**
476 * Make sure filters are suppressed
477 */
478 $query->query['suppress_filters'] = false;
479 $query->set( 'suppress_filters', false );
480
481 $this->maybe_set_search_fields( $query );
482 }
483
484 /**
485 * Handle calls to OrdersAutosuggest methods
486 *
487 * @param string $method_name The method name
488 * @param array $arguments Array of arguments
489 */
490 public function __call( $method_name, $arguments ) {
491 $orders_autosuggest_methods = [
492 'after_update_feature',
493 'check_token_permission',
494 'enqueue_admin_assets',
495 'epio_delete_search_template',
496 'epio_get_search_template',
497 'epio_save_search_template',
498 'filter_term_suggest',
499 'get_args_schema',
500 'get_search_endpoint',
501 'get_search_template',
502 'get_template_endpoint',
503 'get_token',
504 'get_token_endpoint',
505 'intercept_search_request',
506 'is_integrated_request',
507 'post_statuses',
508 'post_types',
509 'mapping',
510 'maybe_query_password_protected_posts',
511 'maybe_set_posts_where',
512 'refresh_token',
513 'rest_api_init',
514 'set_search_fields',
515 ];
516
517 if ( in_array( $method_name, $orders_autosuggest_methods, true ) ) {
518 _deprecated_function(
519 "\ElasticPress\Feature\WooCommerce\WooCommerce\Orders::{$method_name}", // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
520 '4.7.0',
521 "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders_autosuggest->{$method_name}()" // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
522 );
523
524 if ( $this->woocommerce->orders_autosuggest->is_enabled() && method_exists( $this->woocommerce->orders_autosuggest, $method_name ) ) {
525 call_user_func_array( [ $this->woocommerce->orders_autosuggest, $method_name ], $arguments );
526 }
527 }
528 }
529 }
530