PluginProbe
ElasticPress / 4.5.2
ElasticPress v4.5.2
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 / WooCommerce.php

WooCommerce.php in ElasticPress 4.5.2, at includes/classes/Feature/WooCommerce/WooCommerce.php

1,369 lines 41.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ElasticPress WooCommerce feature
4 *
5 * @since 2.1
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Feature\WooCommerce;
10
11 use ElasticPress\Feature;
12 use ElasticPress\FeatureRequirementsStatus;
13 use ElasticPress\Indexables;
14 use ElasticPress\IndexHelper;
15 use ElasticPress\Utils;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit; // Exit if accessed directly.
19 }
20
21 /**
22 * WooCommerce feature class
23 */
24 class WooCommerce extends Feature {
25 /**
26 * If enabled, receive the Orders object instance
27 *
28 * @since 4.5.0
29 * @var null|Orders
30 */
31 public $orders = null;
32
33 /**
34 * Initialize feature setting it's config
35 *
36 * @since 3.0
37 */
38 public function __construct() {
39 $this->slug = 'woocommerce';
40
41 $this->title = esc_html__( 'WooCommerce', 'elasticpress' );
42
43 $this->summary = __( '“I want a cotton, woman’s t-shirt, for under $15 that’s in stock.” Faceted product browsing strains servers and increases load times. Your buyers can find the perfect product quickly, and buy it quickly.', 'elasticpress' );
44
45 $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#woocommerce', 'elasticpress' );
46
47 $this->requires_install_reindex = true;
48
49 $this->setting_requires_install_reindex = 'orders';
50
51 $this->available_during_installation = true;
52
53 $this->default_settings = [
54 'orders' => '0',
55 ];
56
57 $this->orders = new Orders();
58
59 parent::__construct();
60 }
61
62 /**
63 * Index Woocommerce meta
64 *
65 * @param array $meta Existing post meta.
66 * @param array $post Post arguments array.
67 * @since 2.1
68 * @return array
69 */
70 public function whitelist_meta_keys( $meta, $post ) {
71 return array_unique(
72 array_merge(
73 $meta,
74 array(
75 '_thumbnail_id',
76 '_product_attributes',
77 '_wpb_vc_js_status',
78 '_swatch_type',
79 'total_sales',
80 '_downloadable',
81 '_virtual',
82 '_regular_price',
83 '_sale_price',
84 '_tax_status',
85 '_tax_class',
86 '_purchase_note',
87 '_featured',
88 '_weight',
89 '_length',
90 '_width',
91 '_height',
92 '_visibility',
93 '_sku',
94 '_sale_price_dates_from',
95 '_sale_price_dates_to',
96 '_price',
97 '_sold_individually',
98 '_manage_stock',
99 '_backorders',
100 '_stock',
101 '_upsell_ids',
102 '_crosssell_ids',
103 '_stock_status',
104 '_product_version',
105 '_product_tabs',
106 '_override_tab_layout',
107 '_suggested_price',
108 '_min_price',
109 '_customer_user',
110 '_variable_billing',
111 '_wc_average_rating',
112 '_product_image_gallery',
113 '_bj_lazy_load_skip_post',
114 '_min_variation_price',
115 '_max_variation_price',
116 '_min_price_variation_id',
117 '_max_price_variation_id',
118 '_min_variation_regular_price',
119 '_max_variation_regular_price',
120 '_min_regular_price_variation_id',
121 '_max_regular_price_variation_id',
122 '_min_variation_sale_price',
123 '_max_variation_sale_price',
124 '_min_sale_price_variation_id',
125 '_max_sale_price_variation_id',
126 '_default_attributes',
127 '_swatch_type_options',
128 '_order_key',
129 '_billing_company',
130 '_billing_address_1',
131 '_billing_address_2',
132 '_billing_city',
133 '_billing_postcode',
134 '_billing_country',
135 '_billing_state',
136 '_billing_email',
137 '_billing_phone',
138 '_shipping_address_1',
139 '_shipping_address_2',
140 '_shipping_city',
141 '_shipping_postcode',
142 '_shipping_country',
143 '_shipping_state',
144 '_billing_last_name',
145 '_billing_first_name',
146 '_shipping_first_name',
147 '_shipping_last_name',
148 '_variations_skus',
149 )
150 )
151 );
152 }
153
154 /**
155 * Make sure all loop shop post ins are IDS. We have to pass post objects here since we override
156 * the fields=>id query for the layered filter nav query
157 *
158 * @param array $posts Post object array.
159 * @since 2.1
160 * @return array
161 */
162 public function convert_post_object_to_id( $posts ) {
163 _doing_it_wrong( __METHOD__, 'This filter was removed from WooCommerce and will be removed from ElasticPress in a future release.', '4.5.0' );
164 return $posts;
165 }
166
167 /**
168 * Index Woocommerce taxonomies
169 *
170 * @param array $taxonomies Index taxonomies array.
171 * @param array $post Post properties array.
172 * @since 2.1
173 * @return array
174 */
175 public function whitelist_taxonomies( $taxonomies, $post ) {
176 $woo_taxonomies = [];
177
178 $product_type = get_taxonomy( 'product_type' );
179 if ( false !== $product_type ) {
180 $woo_taxonomies[] = $product_type;
181 }
182
183 $product_visibility = get_taxonomy( 'product_visibility' );
184 if ( false !== $product_visibility ) {
185 $woo_taxonomies[] = $product_visibility;
186 }
187
188 /**
189 * Note product_shipping_class, product_cat, and product_tag are already public. Make
190 * sure to index non-attribute taxonomies.
191 */
192 $attribute_taxonomies = wc_get_attribute_taxonomies();
193
194 if ( ! empty( $attribute_taxonomies ) ) {
195 foreach ( $attribute_taxonomies as $tax ) {
196 $name = wc_attribute_taxonomy_name( $tax->attribute_name );
197
198 if ( ! empty( $name ) ) {
199 if ( empty( $tax->attribute_ ) ) {
200 $woo_taxonomies[] = get_taxonomy( $name );
201 }
202 }
203 }
204 }
205
206 return array_merge( $taxonomies, $woo_taxonomies );
207 }
208
209 /**
210 * Disallow duplicated ES queries on Orders page.
211 *
212 * @since 2.4
213 *
214 * @param array $value Original filter values.
215 * @param WP_Query $query WP_Query
216 *
217 * @return array
218 */
219 public function disallow_duplicated_query( $value, $query ) {
220 _doing_it_wrong( __METHOD__, 'This filter was removed from WooCommerce and will be removed from ElasticPress in a future release.', '4.5.0' );
221
222 return $value;
223 }
224
225 /**
226 * Translate args to ElasticPress compat format. This is the meat of what the feature does
227 *
228 * @param WP_Query $query WP Query
229 * @since 2.1
230 */
231 public function translate_args( $query ) {
232 if ( ! $this->should_integrate_with_query( $query ) ) {
233 return;
234 }
235
236 // Flag to check and make sure we are in a WooCommerce specific query
237 $integrate = false;
238
239 /**
240 * Force ElasticPress if we are querying WC taxonomy
241 */
242 $tax_query = $query->get( 'tax_query', [] );
243
244 $supported_taxonomies = array(
245 'product_cat',
246 'product_tag',
247 'product_type',
248 'product_visibility',
249 'product_shipping_class',
250 );
251
252 // Add in any attribute taxonomies that exist
253 $attribute_taxonomies = wc_get_attribute_taxonomy_names();
254
255 $supported_taxonomies = array_merge( $supported_taxonomies, $attribute_taxonomies );
256
257 /**
258 * Filter supported custom taxonomies for WooCommerce integration
259 *
260 * @param {array} $supported_taxonomies An array of default taxonomies.
261 * @hook ep_woocommerce_supported_taxonomies
262 * @since 2.3.0
263 * @return {array} New taxonomies
264 */
265 $supported_taxonomies = apply_filters( 'ep_woocommerce_supported_taxonomies', $supported_taxonomies );
266
267 if ( ! empty( $tax_query ) ) {
268
269 /**
270 * First check if already set taxonomies are supported WC taxes
271 */
272 foreach ( $tax_query as $taxonomy_array ) {
273 if ( isset( $taxonomy_array['taxonomy'] ) && in_array( $taxonomy_array['taxonomy'], $supported_taxonomies, true ) ) {
274 $integrate = true;
275 }
276 }
277 }
278
279 /**
280 * Next check if any taxonomies are in the root of query vars (shorthand form)
281 */
282 foreach ( $supported_taxonomies as $taxonomy ) {
283 $term = $query->get( $taxonomy, false );
284
285 if ( ! empty( $term ) ) {
286 $integrate = true;
287
288 $tax_query[] = array(
289 'taxonomy' => $taxonomy,
290 'field' => 'slug',
291 'terms' => (array) $term,
292 );
293 }
294 }
295
296 /**
297 * Force ElasticPress if product post type query
298 */
299 $post_type = $query->get( 'post_type', false );
300
301 // Act only on a defined subset of all indexable post types here
302 $post_types = array(
303 'shop_order',
304 'shop_order_refund',
305 'product_variation',
306 );
307
308 $is_main_post_type_archive = $query->is_main_query() && $query->is_post_type_archive( 'product' );
309 $has_ep_integrate_set_true = isset( $query->query_vars['ep_integrate'] ) && filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN );
310 if ( $is_main_post_type_archive || $has_ep_integrate_set_true ) {
311 $post_types[] = 'product';
312 }
313
314 /**
315 * Expands or contracts the post_types eligible for indexing.
316 *
317 * @hook ep_woocommerce_default_supported_post_types
318 * @since 4.4.0
319 * @param {array} $post_types Post types
320 * @return {array} New post types
321 */
322 $supported_post_types = apply_filters( 'ep_woocommerce_default_supported_post_types', $post_types );
323
324 $supported_post_types = array_intersect(
325 $supported_post_types,
326 Indexables::factory()->get( 'post' )->get_indexable_post_types()
327 );
328
329 // For orders it queries an array of shop_order and shop_order_refund post types, hence an array_diff
330 if ( ! empty( $post_type ) && ( in_array( $post_type, $supported_post_types, true ) || ( is_array( $post_type ) && ! array_diff( $post_type, $supported_post_types ) ) ) ) {
331 $integrate = true;
332 }
333
334 /**
335 * If we have a WooCommerce specific query, lets hook it to ElasticPress and make the query ElasticSearch friendly
336 */
337 if ( ! $integrate ) {
338 return;
339 }
340
341 // Set tax_query again since we may have added things
342 $query->set( 'tax_query', $tax_query );
343
344 // Default to product if no post type is set
345 if ( empty( $post_type ) ) {
346 $post_type = 'product';
347 $query->set( 'post_type', 'product' );
348 }
349
350 // Handles the WC Top Rated Widget
351 if ( has_filter( 'posts_clauses', array( WC()->query, 'order_by_rating_post_clauses' ) ) ) {
352 remove_filter( 'posts_clauses', array( WC()->query, 'order_by_rating_post_clauses' ) );
353 $query->set( 'orderby', 'meta_value_num' );
354 $query->set( 'meta_key', '_wc_average_rating' );
355 }
356
357 /**
358 * WordPress have to be version 4.6 or newer to have "fields" support
359 * since it requires the "posts_pre_query" filter.
360 *
361 * @see WP_Query::get_posts
362 */
363 $fields = $query->get( 'fields', false );
364 if ( ! version_compare( get_bloginfo( 'version' ), '4.6', '>=' ) && ( 'ids' === $fields || 'id=>parent' === $fields ) ) {
365 $query->set( 'fields', 'default' );
366 }
367
368 /**
369 * Handle meta queries
370 */
371 $meta_query = $query->get( 'meta_query', [] );
372 $meta_key = $query->get( 'meta_key', false );
373 $meta_value = $query->get( 'meta_value', false );
374
375 if ( ! empty( $meta_key ) && ! empty( $meta_value ) ) {
376 $meta_query[] = array(
377 'key' => $meta_key,
378 'value' => $meta_value,
379 );
380
381 $query->set( 'meta_query', $meta_query );
382 }
383
384 /**
385 * Make sure filters are suppressed
386 */
387 $query->query['suppress_filters'] = false;
388 $query->set( 'suppress_filters', false );
389
390 // Integrate with WooCommerce custom searches as well
391 $search = $query->get( 'search' );
392 if ( ! empty( $search ) ) {
393 $s = $search;
394 $query->set( 's', $s );
395 } else {
396 $s = $query->get( 's' );
397 }
398
399 $query->query_vars['ep_integrate'] = true;
400 $query->query['ep_integrate'] = true;
401
402 if ( ! empty( $s ) ) {
403
404 $searchable_post_types = $this->get_admin_searchable_post_types();
405
406 if ( in_array( $post_type, $searchable_post_types, true ) ) {
407 $default_search_fields = array( 'post_title', 'post_content', 'post_excerpt' );
408 if ( ctype_digit( $s ) ) {
409 $default_search_fields[] = 'ID';
410 }
411 $search_fields = $query->get( 'search_fields', $default_search_fields );
412
413 $search_fields['meta'] = array_map(
414 'wc_clean',
415 /**
416 * Filter shop order meta fields to search for WooCommerce
417 *
418 * @hook shop_order_search_fields
419 * @param {array} $fields Shop order fields
420 * @return {array} New fields
421 */
422 apply_filters(
423 'shop_order_search_fields',
424 array(
425 '_order_key',
426 '_billing_company',
427 '_billing_address_1',
428 '_billing_address_2',
429 '_billing_city',
430 '_billing_postcode',
431 '_billing_country',
432 '_billing_state',
433 '_billing_email',
434 '_billing_phone',
435 '_shipping_address_1',
436 '_shipping_address_2',
437 '_shipping_city',
438 '_shipping_postcode',
439 '_shipping_country',
440 '_shipping_state',
441 '_billing_last_name',
442 '_billing_first_name',
443 '_shipping_first_name',
444 '_shipping_last_name',
445 '_items',
446 )
447 )
448 );
449
450 $query->set(
451 'search_fields',
452 /**
453 * Filter all the shop order fields to search for WooCommerce
454 *
455 * @hook ep_woocommerce_shop_order_search_fields
456 * @since 4.0.0
457 * @param {array} $fields Shop order fields
458 * @param {WP_Query} $query WP Query
459 * @return {array} New fields
460 */
461 apply_filters( 'ep_woocommerce_shop_order_search_fields', $search_fields, $query )
462 );
463 } elseif ( 'product' === $post_type && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) {
464 $search_fields = $query->get( 'search_fields', array( 'post_title', 'post_content', 'post_excerpt' ) );
465
466 // Remove author_name from this search.
467 $search_fields = $this->remove_author( $search_fields );
468
469 foreach ( $search_fields as $field_key => $field ) {
470 if ( 'author_name' === $field ) {
471 unset( $search_fields[ $field_key ] );
472 }
473 }
474
475 $search_fields['meta'] = ( ! empty( $search_fields['meta'] ) ) ? $search_fields['meta'] : [];
476 $search_fields['taxonomies'] = ( ! empty( $search_fields['taxonomies'] ) ) ? $search_fields['taxonomies'] : [];
477
478 $search_fields['meta'] = array_merge( $search_fields['meta'], array( '_sku' ) );
479 $search_fields['taxonomies'] = array_merge( $search_fields['taxonomies'], array( 'category', 'post_tag', 'product_tag', 'product_cat' ) );
480
481 $query->set( 'search_fields', $search_fields );
482 }
483 } else {
484 /**
485 * For default sorting by popularity (total_sales) and rating
486 * Woocommerce doesn't set the orderby correctly.
487 * These lines will check the meta_key and correct the orderby based on that.
488 * And this won't run in search result and only run in main query
489 */
490 $meta_key = $query->get( 'meta_key', false );
491 if ( $meta_key && $query->is_main_query() ) {
492 switch ( $meta_key ) {
493 case 'total_sales':
494 $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) );
495 $query->set( 'order', 'DESC' );
496 break;
497 case '_wc_average_rating':
498 $query->set( 'orderby', $this->get_orderby_meta_mapping( '_wc_average_rating' ) );
499 $query->set( 'order', 'DESC' );
500 break;
501 }
502 }
503 }
504
505 /**
506 * Set orderby and order for price/popularity when GET param not set
507 */
508 if ( isset( $query->query_vars['orderby'], $query->query_vars['order'] ) && $query->is_main_query() ) {
509 switch ( $query->query_vars['orderby'] ) {
510 case 'price':
511 $query->set( 'order', $query->query_vars['order'] );
512 $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) );
513 break;
514 case 'popularity':
515 $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) );
516 $query->set( 'order', 'DESC' );
517 break;
518 }
519 }
520
521 /**
522 * Set orderby from GET param
523 * Also make sure the orderby param affects only the main query
524 */
525 if ( ! empty( $_GET['orderby'] ) && $query->is_main_query() ) { // phpcs:ignore WordPress.Security.NonceVerification
526 $orderby = sanitize_text_field( $_GET['orderby'] ); // phpcs:ignore WordPress.Security.NonceVerification
527 switch ( $orderby ) { // phpcs:ignore WordPress.Security.NonceVerification
528 case 'popularity':
529 $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) );
530 $query->set( 'order', 'DESC' );
531 break;
532 case 'price':
533 $query->set( 'order', $query->get( 'order', 'ASC' ) );
534 $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) );
535 break;
536 case 'price-desc':
537 $query->set( 'order', 'DESC' );
538 $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) );
539 break;
540 case 'rating':
541 $query->set( 'orderby', $this->get_orderby_meta_mapping( '_wc_average_rating' ) );
542 $query->set( 'order', 'DESC' );
543 break;
544 case 'date':
545 case 'title':
546 case 'ID':
547 $query->set( 'orderby', $this->get_orderby_meta_mapping( $orderby ) );
548 break;
549 case 'sku':
550 $query->set( 'orderby', $this->get_orderby_meta_mapping( '_sku' ) );
551 break;
552 default:
553 $query->set( 'orderby', $this->get_orderby_meta_mapping( 'menu_order' ) ); // Order by menu and title.
554 }
555 }
556 }
557
558 /**
559 * Fetch the ES related meta mapping for orderby
560 *
561 * @param array $meta_key The meta key to get the mapping for.
562 * @since 2.1
563 * @return string The mapped meta key.
564 */
565 public function get_orderby_meta_mapping( $meta_key ) {
566 /**
567 * Filter WooCommerce to Elasticsearch meta mapping
568 *
569 * @hook orderby_meta_mapping
570 * @param {array} $mapping Meta mapping
571 * @return {array} New mapping
572 */
573 $mapping = apply_filters(
574 'orderby_meta_mapping',
575 array(
576 'ID' => 'ID',
577 'title' => 'title date',
578 'menu_order' => 'menu_order title date',
579 'menu_order title' => 'menu_order title date',
580 'total_sales' => 'meta.total_sales.double date',
581 '_wc_average_rating' => 'meta._wc_average_rating.double date',
582 '_price' => 'meta._price.double date',
583 '_sku' => 'meta._sku.value.sortable date',
584 )
585 );
586
587 if ( isset( $mapping[ $meta_key ] ) ) {
588 return $mapping[ $meta_key ];
589 }
590
591 return 'date';
592 }
593
594
595 /**
596 * Returns the WooCommerce-oriented post types in admin that EP will search
597 *
598 * @since 4.4.0
599 * @return mixed|void
600 */
601 public function get_admin_searchable_post_types() {
602 $searchable_post_types = array( 'shop_order' );
603
604 /**
605 * Filter admin searchable WooCommerce post types
606 *
607 * @hook ep_woocommerce_admin_searchable_post_types
608 * @since 4.4.0
609 * @param {array} $post_types Post types
610 * @return {array} New post types
611 */
612 return apply_filters( 'ep_woocommerce_admin_searchable_post_types', $searchable_post_types );
613 }
614
615 /**
616 * Make search coupons don't go through ES
617 *
618 * @param bool $enabled Coupons enabled or not
619 * @param WP_Query $query WP Query
620 * @since 2.1
621 * @return bool
622 */
623 public function blacklist_coupons( $enabled, $query ) {
624 if ( is_admin() ) {
625 return $enabled;
626 }
627
628 if ( 'shop_coupon' === $query->get( 'post_type' ) && empty( $query->query_vars['ep_integrate'] ) ) {
629 return false;
630 }
631
632 return $enabled;
633 }
634
635 /**
636 * Allow order creations on the front end to get synced
637 *
638 * @since 2.1
639 * @param bool $override Original order perms check value
640 * @param int $post_id Post ID
641 * @return bool
642 */
643 public function bypass_order_permissions_check( $override, $post_id ) {
644 $searchable_post_types = $this->get_admin_searchable_post_types();
645
646 if ( in_array( get_post_type( $post_id ), $searchable_post_types, true ) ) {
647 return true;
648 }
649
650 return $override;
651 }
652
653 /**
654 * Sets woocommerce meta search fields to an empty array if we are integrating the main query with ElasticSearch
655 *
656 * Woocommerce calls this action as part of its own callback on parse_query. We add this filter only if the query
657 * is integrated with ElasticSearch.
658 * If we were to always return array() on this filter, we'd break admin searches when WooCommerce module is activated
659 * without the Protected Content Module
660 *
661 * @param \WP_Query $query Current query
662 */
663 public function maybe_hook_woocommerce_search_fields( $query ) {
664 global $pagenow, $wp, $wc_list_table, $wp_filter;
665
666 if ( ! $this->should_integrate_with_query( $query ) ) {
667 return;
668 }
669
670 /**
671 * Determines actions to be applied, or removed, if doing a WooCommerce serarch
672 *
673 * @hook ep_woocommerce_hook_search_fields
674 * @since 4.4.0
675 */
676 do_action( 'ep_woocommerce_hook_search_fields' );
677
678 if ( 'edit.php' !== $pagenow || empty( $wp->query_vars['s'] ) || 'shop_order' !== $wp->query_vars['post_type'] || ! isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
679 return;
680 }
681
682 remove_action( 'parse_query', [ $wc_list_table, 'search_custom_fields' ] );
683 }
684
685 /**
686 * Enhance WooCommerce search order by order id, email, phone number, name, etc..
687 * What this function does:
688 * 1. Reverse the woocommerce shop_order_search_custom_fields query
689 * 2. If the search key is integer and it is an Order Id, just query with post__in
690 * 3. If the search key is integer but not an order id ( might be phone number ), use ES to find it
691 *
692 * @param WP_Query $wp WP Query
693 * @since 2.3
694 */
695 public function search_order( $wp ) {
696 if ( ! $this->should_integrate_with_query( $wp ) ) {
697 return;
698 }
699
700 global $pagenow;
701
702 $searchable_post_types = $this->get_admin_searchable_post_types();
703
704 if ( 'edit.php' !== $pagenow || empty( $wp->query_vars['post_type'] ) || ! in_array( $wp->query_vars['post_type'], $searchable_post_types, true ) ||
705 ( empty( $wp->query_vars['s'] ) && empty( $wp->query_vars['shop_order_search'] ) ) ) {
706 return;
707 }
708
709 $search_key_safe = str_replace( array( 'Order #', '#' ), '', wc_clean( $_GET['s'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
710 unset( $wp->query_vars['post__in'] );
711 $wp->query_vars['s'] = $search_key_safe;
712 }
713
714 /**
715 * Add order items as a searchable string.
716 *
717 * This mimics how WooCommerce currently does in the order_itemmeta
718 * table. They combine the titles of the products and put them in a
719 * meta field called "Items".
720 *
721 * @since 2.4
722 *
723 * @param array $post_args Post arguments
724 * @param string|int $post_id Post id
725 *
726 * @return array
727 */
728 public function add_order_items_search( $post_args, $post_id ) {
729 $searchable_post_types = $this->get_admin_searchable_post_types();
730
731 // Make sure it is only WooCommerce orders we touch.
732 if ( ! in_array( $post_args['post_type'], $searchable_post_types, true ) ) {
733 return $post_args;
734 }
735
736 $post_indexable = Indexables::factory()->get( 'post' );
737
738 // Get order items.
739 $order = wc_get_order( $post_id );
740 $item_meta = [];
741 foreach ( $order->get_items() as $delta => $product_item ) {
742 // WooCommerce 3.x uses WC_Order_Item_Product instance while 2.x an array
743 if ( is_object( $product_item ) && method_exists( $product_item, 'get_name' ) ) {
744 $item_meta['_items'][] = $product_item->get_name( 'edit' );
745 } elseif ( is_array( $product_item ) && isset( $product_item['name'] ) ) {
746 $item_meta['_items'][] = $product_item['name'];
747 }
748 }
749
750 // Prepare order items.
751 $item_meta['_items'] = empty( $item_meta['_items'] ) ? '' : implode( '|', $item_meta['_items'] );
752 $post_args['meta'] = array_merge( $post_args['meta'], $post_indexable->prepare_meta_types( $item_meta ) );
753
754 return $post_args;
755 }
756
757 /**
758 * Add WooCommerce Product Attributes to EP Facets.
759 *
760 * @param array $taxonomies Taxonomies array
761 * @return array
762 */
763 public function add_product_attributes( $taxonomies = [] ) {
764 $attribute_names = wc_get_attribute_taxonomy_names();
765
766 foreach ( $attribute_names as $name ) {
767 if ( ! taxonomy_exists( $name ) ) {
768 continue;
769 }
770 $taxonomies[ $name ] = get_taxonomy( $name );
771 }
772
773 return $taxonomies;
774 }
775
776 /**
777 * Add WooCommerce Fields to the Weighting Dashboard.
778 *
779 * @since 3.x
780 *
781 * @param array $fields Current weighting fields.
782 * @param string $post_type Current post type.
783 * @return array New fields.
784 */
785 public function add_product_attributes_to_weighting( $fields, $post_type ) {
786 if ( 'product' === $post_type ) {
787 if ( ! empty( $fields['attributes']['children']['author_name'] ) ) {
788 unset( $fields['attributes']['children']['author_name'] );
789 }
790
791 $sku_key = 'meta._sku.value';
792
793 $fields['attributes']['children'][ $sku_key ] = array(
794 'key' => $sku_key,
795 'label' => __( 'SKU', 'elasticpress' ),
796 );
797
798 $variations_skus_key = 'meta._variations_skus.value';
799
800 $fields['attributes']['children'][ $variations_skus_key ] = array(
801 'key' => $variations_skus_key,
802 'label' => __( 'Variations SKUs', 'elasticpress' ),
803 );
804 }
805 return $fields;
806 }
807
808 /**
809 * Add WooCommerce Fields to the default values of the Weighting Dashboard.
810 *
811 * @since 3.x
812 *
813 * @param array $defaults Default values for the post type.
814 * @param string $post_type Current post type.
815 * @return array
816 */
817 public function add_product_default_post_type_weights( $defaults, $post_type ) {
818 if ( 'product' === $post_type ) {
819 if ( ! empty( $defaults['author_name'] ) ) {
820 unset( $defaults['author_name'] );
821 }
822
823 $defaults['meta._sku.value'] = array(
824 'enabled' => true,
825 'weight' => 1,
826 );
827
828 $defaults['meta._variations_skus.value'] = array(
829 'enabled' => true,
830 'weight' => 1,
831 );
832 }
833 return $defaults;
834 }
835
836 /**
837 * Add WC post type to autosuggest
838 *
839 * @param array $post_types Array of post types (e.g. post, page).
840 * @since 2.6
841 * @return array
842 */
843 public function suggest_wc_add_post_type( $post_types ) {
844 if ( ! in_array( 'product', $post_types, true ) ) {
845 $post_types[] = 'product';
846 }
847
848 return $post_types;
849 }
850
851 /**
852 * Setup all feature filters
853 *
854 * @since 2.1
855 */
856 public function setup() {
857 if ( ! function_exists( 'WC' ) ) {
858 return;
859 }
860
861 add_action( 'ep_formatted_args', [ $this, 'price_filter' ], 10, 3 );
862 add_filter( 'ep_sync_insert_permissions_bypass', [ $this, 'bypass_order_permissions_check' ], 10, 2 );
863 add_filter( 'ep_integrate_search_queries', [ $this, 'blacklist_coupons' ], 10, 2 );
864 add_filter( 'ep_prepare_meta_allowed_protected_keys', [ $this, 'whitelist_meta_keys' ], 10, 2 );
865 add_filter( 'woocommerce_layered_nav_query_post_ids', [ $this, 'convert_post_object_to_id' ], 10, 4 );
866 add_filter( 'woocommerce_unfiltered_product_ids', [ $this, 'convert_post_object_to_id' ], 10, 4 );
867 add_filter( 'ep_sync_taxonomies', [ $this, 'whitelist_taxonomies' ], 10, 2 );
868 add_filter( 'ep_post_sync_args_post_prepare_meta', [ $this, 'add_order_items_search' ], 20, 2 );
869 add_filter( 'ep_pc_skip_post_content_cleanup', [ $this, 'keep_order_fields' ], 20, 2 );
870 add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 );
871 add_action( 'ep_wp_query_search_cached_posts', [ $this, 'disallow_duplicated_query' ], 10, 2 );
872 add_action( 'parse_query', [ $this, 'maybe_hook_woocommerce_search_fields' ], 1 );
873 add_action( 'parse_query', [ $this, 'search_order' ], 11 );
874 add_filter( 'ep_term_suggest_post_type', [ $this, 'suggest_wc_add_post_type' ] );
875 add_filter( 'ep_facet_include_taxonomies', [ $this, 'add_product_attributes' ] );
876 add_filter( 'ep_weighting_fields_for_post_type', [ $this, 'add_product_attributes_to_weighting' ], 10, 2 );
877 add_filter( 'ep_weighting_default_post_type_weights', [ $this, 'add_product_default_post_type_weights' ], 10, 2 );
878 add_filter( 'ep_prepare_meta_data', [ $this, 'add_variations_skus_meta' ], 10, 2 );
879 add_filter( 'request', [ $this, 'admin_product_list_request_query' ], 9 );
880
881 // Custom product ordering
882 add_action( 'ep_admin_notices', [ $this, 'maybe_display_notice_about_product_ordering' ] );
883 add_action( 'woocommerce_after_product_ordering', [ $this, 'action_sync_on_woocommerce_sort_single' ], 10, 2 );
884
885 // Orders Autosuggest feature.
886 if ( $this->is_orders_autosuggest_enabled() ) {
887 $this->orders->setup();
888 }
889 }
890
891 /**
892 * Output feature box long
893 *
894 * @since 2.1
895 */
896 public function output_feature_box_long() {
897 ?>
898 <p><?php esc_html_e( 'Most caching and performance tools can’t keep up with the nearly infinite ways your visitors might filter or navigate your products. No matter how many products, filters, or customers you have, ElasticPress will keep your online store performing quickly. If used in combination with the Protected Content feature, ElasticPress will also accelerate order searches and back end product management.', 'elasticpress' ); ?></p>
899 <?php
900 }
901
902 /**
903 * Dashboard WooCommerce settings
904 *
905 * @since 4.5.0
906 */
907 public function output_feature_box_settings() {
908 $available = $this->is_orders_autosuggest_available();
909 $enabled = $this->is_orders_autosuggest_enabled();
910 ?>
911 <div class="field">
912 <div class="field-name status"><?php esc_html_e( 'Orders Autosuggest', 'elasticpress' ); ?></div>
913 <div class="input-wrap">
914 <label><input name="settings[orders]" type="radio" <?php checked( $enabled ); ?> <?php disabled( $available, false, true ); ?> value="1"><?php echo wp_kses_post( __( 'Enabled', 'elasticpress' ) ); ?></label><br>
915 <label><input name="settings[orders]" type="radio" <?php checked( ! $enabled ); ?> <?php disabled( $available, false, true ); ?> value="0"><?php echo wp_kses_post( __( 'Disabled', 'elasticpress' ) ); ?></label>
916 <p class="field-description">
917 <?php
918 $epio_autosuggest_kb_link = 'https://elasticpress.zendesk.com/hc/en-us/articles/13374461690381-Configuring-ElasticPress-io-Order-Autosuggest';
919
920 $message = ( $available ) ?
921 /* translators: 1: <a> tag (ElasticPress.io); 2. </a>; 3: <a> tag (KB article); 4. </a>; */
922 __( 'You are directly connected to %1$sElasticPress.io%2$s! Enable Orders Autosuggest to enhance Dashboard results and quickly find WooCommerce Orders. %3$sLearn More%4$s.', 'elasticpress' ) :
923 /* translators: 1: <a> tag (ElasticPress.io); 2. </a>; 3: <a> tag (KB article); 4. </a>; */
924 __( 'Due to the sensitive nature of orders, this autosuggest feature is available only to %1$sElasticPress.io%2$s customers. %3$sLearn More%4$s.', 'elasticpress' );
925
926 printf(
927 wp_kses( $message, 'ep-html' ),
928 '<a href="https://elasticpress.io/" target="_blank">',
929 '</a>',
930 '<a href="' . esc_url( $epio_autosuggest_kb_link ) . '" target="_blank">',
931 '</a>'
932 );
933 ?>
934 </p>
935 </div>
936 </div>
937 <?php
938 }
939
940 /**
941 * Remove the author_name from search fields.
942 *
943 * @param array $search_fields Array of search fields.
944 * @since 3.0
945 * @return array
946 */
947 public function remove_author( $search_fields ) {
948 foreach ( $search_fields as $field_key => $field ) {
949 if ( 'author_name' === $field ) {
950 unset( $search_fields[ $field_key ] );
951 }
952 }
953
954 return $search_fields;
955 }
956
957 /**
958 * Determine WC feature reqs status
959 *
960 * @since 2.2
961 * @return EP_Feature_Requirements_Status
962 */
963 public function requirements_status() {
964 $status = new FeatureRequirementsStatus( 0 );
965
966 if ( ! class_exists( 'WooCommerce' ) ) {
967 $status->code = 2;
968 $status->message = esc_html__( 'WooCommerce not installed.', 'elasticpress' );
969 }
970
971 return $status;
972 }
973
974 /**
975 * Modifies main query to allow filtering by price with WooCommerce "Filter by price" widget.
976 *
977 * @param array $args ES args
978 * @param array $query_args WP_Query args
979 * @param WP_Query $query WP_Query object
980 * @since 3.2
981 * @return array
982 */
983 public function price_filter( $args, $query_args, $query ) {
984 // Only can use widget on main query
985 if ( ! $query->is_main_query() ) {
986 return $args;
987 }
988
989 // Only can use widget on shop, product taxonomy, or search
990 if ( ! is_shop() && ! is_product_taxonomy() && ! is_search() ) {
991 return $args;
992 }
993
994 // phpcs:disable WordPress.Security.NonceVerification
995 if ( empty( $_GET['min_price'] ) && empty( $_GET['max_price'] ) ) {
996 return $args;
997 }
998
999 if ( $query->is_search() ) {
1000 /**
1001 * This logic is iffy but the WC price filter widget is not intended for use with search anyway
1002 */
1003 $old_query = $args['query']['bool'];
1004 unset( $args['query']['bool']['should'] );
1005
1006 if ( ! empty( $_GET['min_price'] ) ) {
1007 $args['query']['bool']['must'][0]['range']['meta._price.long']['gte'] = $_GET['min_price'];
1008 }
1009
1010 if ( ! empty( $_GET['max_price'] ) ) {
1011 $args['query']['bool']['must'][0]['range']['meta._price.long']['lte'] = $_GET['max_price'];
1012 }
1013
1014 $args['query']['bool']['must'][0]['range']['meta._price.long']['boost'] = 2.0;
1015 $args['query']['bool']['must'][1]['bool'] = $old_query;
1016 } else {
1017 unset( $args['query']['match_all'] );
1018
1019 $args['query']['range']['meta._price.long']['gte'] = ! empty( $_GET['min_price'] ) ? $_GET['min_price'] : 0;
1020
1021 if ( ! empty( $_GET['min_price'] ) ) {
1022 $args['query']['range']['meta._price.long']['gte'] = $_GET['min_price'];
1023 }
1024
1025 if ( ! empty( $_GET['max_price'] ) ) {
1026 $args['query']['range']['meta._price.long']['lte'] = $_GET['max_price'];
1027 }
1028
1029 $args['query']['range']['meta._price.long']['boost'] = 2.0;
1030 }
1031 // phpcs:enable WordPress.Security.NonceVerification
1032
1033 return $args;
1034 }
1035
1036 /**
1037 * Prevent order fields from being removed.
1038 *
1039 * When Protected Content is enabled, all posts with password have their content removed.
1040 * This can't happen for orders, as the order key is added in that field.
1041 *
1042 * @see https://github.com/10up/ElasticPress/issues/2726
1043 *
1044 * @since 4.2.0
1045 * @param bool $skip Whether the password protected content should have their content, and meta removed
1046 * @param array $post_args Post arguments
1047 * @return bool
1048 */
1049 public function keep_order_fields( $skip, $post_args ) {
1050 $searchable_post_types = $this->get_admin_searchable_post_types();
1051
1052 if ( in_array( $post_args['post_type'], $searchable_post_types, true ) ) {
1053 return true;
1054 }
1055
1056 return $skip;
1057 }
1058
1059 /**
1060 * Add a new `_variations_skus` meta field to the product to be indexed in Elasticsearch.
1061 *
1062 * @since 4.2.0
1063 * @param array $post_meta Post meta
1064 * @param WP_Post $post Post object
1065 * @return array
1066 */
1067 public function add_variations_skus_meta( $post_meta, $post ) {
1068 if ( 'product' !== $post->post_type ) {
1069 return $post_meta;
1070 }
1071
1072 $product = wc_get_product( $post );
1073 if ( ! $product ) {
1074 return $post_meta;
1075 }
1076
1077 $variations_ids = $product->get_children();
1078
1079 $post_meta['_variations_skus'] = array_reduce(
1080 $variations_ids,
1081 function ( $variations_skus, $current_id ) {
1082 $variation = wc_get_product( $current_id );
1083 if ( ! $variation || ! $variation->exists() ) {
1084 return $variations_skus;
1085 }
1086 $variation_sku = $variation->get_sku();
1087 if ( ! $variation_sku ) {
1088 return $variations_skus;
1089 }
1090 $variations_skus[] = $variation_sku;
1091 return $variations_skus;
1092 },
1093 []
1094 );
1095
1096 return $post_meta;
1097 }
1098
1099 /**
1100 * Integrate ElasticPress with the WooCommerce Admin Product List.
1101 *
1102 * WooCommerce uses its `WC_Admin_List_Table_Products` class to control that screen. This
1103 * function adds all necessary hooks to bypass the default behavior and integrate with ElasticPress.
1104 * By default, WC runs a SQL query to get the Product IDs that match the list criteria and passes
1105 * that list of IDs to the main WP_Query. This integration changes that process to a single query, run
1106 * by ElasticPress.
1107 *
1108 * @since 4.2.0
1109 * @param array $query_vars Query vars.
1110 * @return array
1111 */
1112 public function admin_product_list_request_query( $query_vars ) {
1113 global $typenow, $wc_list_table;
1114
1115 // Return if not in the correct screen.
1116 if ( ! is_a( $wc_list_table, 'WC_Admin_List_Table_Products' ) || 'product' !== $typenow ) {
1117 return $query_vars;
1118 }
1119
1120 // Return if admin WP_Query integration is not turned on, i.e., Protect Content is not enabled.
1121 if ( ! has_filter( 'ep_admin_wp_query_integration', '__return_true' ) ) {
1122 return $query_vars;
1123 }
1124
1125 /**
1126 * Filter to skip integration with WooCommerce Admin Product List.
1127 *
1128 * @hook ep_woocommerce_integrate_admin_products_list
1129 * @since 4.2.0
1130 * @param {bool} $integrate True to integrate, false to preserve original behavior. Defaults to true.
1131 * @param {array} $query_vars Query vars.
1132 * @return {bool} New integrate value
1133 */
1134 if ( ! apply_filters( 'ep_woocommerce_integrate_admin_products_list', true, $query_vars ) ) {
1135 return $query_vars;
1136 }
1137
1138 add_action( 'pre_get_posts', [ $this, 'translate_args_admin_products_list' ], 12 );
1139
1140 // This short-circuits WooCommerce search for product IDs.
1141 add_filter( 'woocommerce_product_pre_search_products', '__return_empty_array' );
1142
1143 return $query_vars;
1144 }
1145
1146 /**
1147 * Apply the necessary changes to WP_Query in WooCommerce Admin Product List.
1148 *
1149 * @param WP_Query $query The WP Query being executed.
1150 */
1151 public function translate_args_admin_products_list( $query ) {
1152 // The `translate_args()` method sets it to `true` if we should integrate it.
1153 if ( ! $query->get( 'ep_integrate', false ) ) {
1154 return;
1155 }
1156
1157 // WooCommerce unsets the search term right after using it to fetch product IDs. Here we add it back.
1158 $search_term = ! empty( $_GET['s'] ) ? sanitize_text_field( $_GET['s'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
1159 if ( ! empty( $search_term ) ) {
1160 $query->set( 's', sanitize_text_field( $search_term ) ); // phpcs:ignore WordPress.Security.NonceVerification
1161
1162 /**
1163 * Filter the fields used in WooCommerce Admin Product Search.
1164 *
1165 * ```
1166 * add_filter(
1167 * 'ep_woocommerce_admin_products_list_search_fields',
1168 * function ( $wc_admin_search_fields ) {
1169 * $wc_admin_search_fields['meta'][] = 'custom_field';
1170 * return $wc_admin_search_fields;
1171 * }
1172 * );
1173 * ```
1174 *
1175 * @hook ep_woocommerce_admin_products_list_search_fields
1176 * @since 4.2.0
1177 * @param {array} $wc_admin_search_fields Fields to be used in the WooCommerce Admin Product Search
1178 * @return {array} New fields
1179 */
1180 $search_fields = apply_filters(
1181 'ep_woocommerce_admin_products_list_search_fields',
1182 [
1183 'post_title',
1184 'post_content',
1185 'post_excerpt',
1186 'meta' => [
1187 '_sku',
1188 '_variations_skus',
1189 ],
1190 ]
1191 );
1192
1193 $query->set( 'search_fields', $search_fields );
1194 }
1195
1196 // Sets the meta query for `product_type` if needed. Also removed from the WP_Query by WC in `WC_Admin_List_Table_Products::query_filters()`.
1197 $product_type_query = $query->get( 'product_type', '' );
1198 $product_type_url = ! empty( $_GET['product_type'] ) ? sanitize_text_field( $_GET['product_type'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
1199 $allowed_prod_types = [ 'virtual', 'downloadable' ];
1200 if ( empty( $product_type_query ) && ! empty( $product_type_url ) && in_array( $product_type_url, $allowed_prod_types, true ) ) {
1201 $meta_query = $query->get( 'meta_query', [] );
1202 $meta_query[] = [
1203 'key' => "_{$product_type_url}",
1204 'value' => 'yes',
1205 ];
1206 $query->set( 'meta_query', $meta_query );
1207 }
1208
1209 // Sets the meta query for `stock_status` if needed.
1210 $stock_status_query = $query->get( 'stock_status', '' );
1211 $stock_status_url = ! empty( $_GET['stock_status'] ) ? sanitize_text_field( $_GET['stock_status'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
1212 $allowed_stock_status = [ 'instock', 'outofstock', 'onbackorder' ];
1213 if ( empty( $stock_status_query ) && ! empty( $stock_status_url ) && in_array( $stock_status_url, $allowed_stock_status, true ) ) {
1214 $meta_query = $query->get( 'meta_query', [] );
1215 $meta_query[] = [
1216 'key' => '_stock_status',
1217 'value' => $stock_status_url,
1218 ];
1219 $query->set( 'meta_query', $meta_query );
1220 }
1221 }
1222
1223 /**
1224 * Determines whether or not ES should be integrating with the provided query
1225 *
1226 * @param \WP_Query $query Query we might integrate with
1227 *
1228 * @return bool
1229 */
1230 protected function should_integrate_with_query( $query ) {
1231 // Lets make sure this doesn't interfere with the CLI
1232 if ( defined( 'WP_CLI' ) && WP_CLI ) {
1233 return false;
1234 }
1235
1236 if ( defined( 'WC_API_REQUEST' ) && WC_API_REQUEST ) {
1237 return false;
1238 }
1239
1240 if ( isset( $query->query_vars['ep_integrate'] ) && ! filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN ) ) {
1241 return false;
1242 }
1243
1244 /**
1245 * Filter to skip WP Query integration
1246 *
1247 * @hook ep_skip_query_integration
1248 * @param {bool} $skip True to skip
1249 * @param {WP_Query} $query WP Query to evaluate
1250 * @return {bool} New skip value
1251 */
1252 if ( apply_filters( 'ep_skip_query_integration', false, $query ) ) {
1253 return false;
1254 }
1255
1256 if ( ! Utils\is_integrated_request( $this->slug ) ) {
1257 return false;
1258 }
1259
1260 /**
1261 * Do nothing for single product queries
1262 */
1263 $product_name = $query->get( 'product', false );
1264 if ( ! empty( $product_name ) || $query->is_single() ) {
1265 return false;
1266 }
1267
1268 /**
1269 * ElasticPress does not yet support post_parent queries
1270 */
1271 $post_parent = $query->get( 'post_parent', false );
1272 if ( ! empty( $post_parent ) ) {
1273 return false;
1274 }
1275
1276 /**
1277 * If this is just a preview, let's not use Elasticsearch.
1278 */
1279 if ( $query->get( 'preview', false ) ) {
1280 return false;
1281 }
1282
1283 return true;
1284 }
1285
1286 /**
1287 * Depending on the number of products display an admin notice in the custom sort screen for WooCommerce Products
1288 *
1289 * @since 4.4.0
1290 * @param array $notices Current ElasticPress admin notices
1291 * @return array
1292 */
1293 public function maybe_display_notice_about_product_ordering( $notices ) {
1294 global $pagenow, $wp_query;
1295
1296 /**
1297 * Make sure we're on edit.php in admin dashboard.
1298 */
1299 if ( ! is_admin() || 'edit.php' !== $pagenow || empty( $wp_query->query['orderby'] ) || 'menu_order title' !== $wp_query->query['orderby'] ) {
1300 return $notices;
1301 }
1302
1303 $documents_per_page_sync = IndexHelper::factory()->get_index_default_per_page();
1304 if ( $documents_per_page_sync >= $wp_query->found_posts ) {
1305 return $notices;
1306 }
1307
1308 $notices['woocommerce_custom_sort'] = [
1309 'html' => sprintf(
1310 /* translators: Sync Page URL */
1311 __( 'Due to the number of products in the site, you will need to <a href="%s">resync</a> after applying a custom sort order.', 'elasticpress' ),
1312 Utils\get_sync_url()
1313 ),
1314 'type' => 'warning',
1315 'dismiss' => true,
1316 ];
1317
1318 return $notices;
1319 }
1320
1321 /**
1322 * Conditionally resync products after applying a custom order.
1323 *
1324 * @since 4.4.0
1325 * @param int $sorting_id ID of post dragged and dropped
1326 * @param array $menu_orders Post IDs and their new menu_order value
1327 */
1328 public function action_sync_on_woocommerce_sort_single( $sorting_id, $menu_orders ) {
1329
1330 $documents_per_page_sync = IndexHelper::factory()->get_index_default_per_page();
1331 if ( $documents_per_page_sync < count( $menu_orders ) ) {
1332 return;
1333 }
1334
1335 $sync_manager = Indexables::factory()->get( 'post' )->sync_manager;
1336 foreach ( $menu_orders as $post_id => $order ) {
1337 $sync_manager->add_to_queue( $post_id );
1338 }
1339 }
1340
1341 /**
1342 * Whether orders autosuggest is available or not
1343 *
1344 * @since 4.5.0
1345 * @return boolean
1346 */
1347 public function is_orders_autosuggest_available() : bool {
1348 /**
1349 * Whether the autosuggest feature is available for non
1350 * ElasticPress.io customers.
1351 *
1352 * @since 4.5.0
1353 * @hook ep_woocommerce_orders_autosuggest_available
1354 * @param {boolean} $available Whether the feature is available.
1355 */
1356 return apply_filters( 'ep_woocommerce_orders_autosuggest_available', Utils\is_epio() );
1357 }
1358
1359 /**
1360 * Whether orders autosuggest is enabled or not
1361 *
1362 * @since 4.5.0
1363 * @return boolean
1364 */
1365 public function is_orders_autosuggest_enabled() : bool {
1366 return $this->is_orders_autosuggest_available() && '1' === $this->get_setting( 'orders' );
1367 }
1368 }
1369