PluginProbe
ElasticPress / 5.0.1
ElasticPress v5.0.1
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.0.1, at includes/classes/Feature/WooCommerce/Orders.php

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