PluginProbe
ElasticPress / 4.6.1
ElasticPress v4.6.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 / WooCommerce.php

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

1,426 lines 44.3 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( wp_unslash( $_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 // phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
710 if ( isset( $_GET['s'] ) ) {
711 $search_key_safe = str_replace( array( 'Order #', '#' ), '', wc_clean( $_GET['s'] ) );
712 unset( $wp->query_vars['post__in'] );
713 $wp->query_vars['s'] = $search_key_safe;
714 }
715 // phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
716 }
717
718 /**
719 * Add order items as a searchable string.
720 *
721 * This mimics how WooCommerce currently does in the order_itemmeta
722 * table. They combine the titles of the products and put them in a
723 * meta field called "Items".
724 *
725 * @since 2.4
726 *
727 * @param array $post_args Post arguments
728 * @param string|int $post_id Post id
729 *
730 * @return array
731 */
732 public function add_order_items_search( $post_args, $post_id ) {
733 $searchable_post_types = $this->get_admin_searchable_post_types();
734
735 // Make sure it is only WooCommerce orders we touch.
736 if ( ! in_array( $post_args['post_type'], $searchable_post_types, true ) ) {
737 return $post_args;
738 }
739
740 $post_indexable = Indexables::factory()->get( 'post' );
741
742 // Get order items.
743 $order = wc_get_order( $post_id );
744 $item_meta = [];
745 foreach ( $order->get_items() as $delta => $product_item ) {
746 // WooCommerce 3.x uses WC_Order_Item_Product instance while 2.x an array
747 if ( is_object( $product_item ) && method_exists( $product_item, 'get_name' ) ) {
748 $item_meta['_items'][] = $product_item->get_name( 'edit' );
749 } elseif ( is_array( $product_item ) && isset( $product_item['name'] ) ) {
750 $item_meta['_items'][] = $product_item['name'];
751 }
752 }
753
754 // Prepare order items.
755 $item_meta['_items'] = empty( $item_meta['_items'] ) ? '' : implode( '|', $item_meta['_items'] );
756 $post_args['meta'] = array_merge( $post_args['meta'], $post_indexable->prepare_meta_types( $item_meta ) );
757
758 return $post_args;
759 }
760
761 /**
762 * Add WooCommerce Product Attributes to EP Facets.
763 *
764 * @param array $taxonomies Taxonomies array
765 * @return array
766 */
767 public function add_product_attributes( $taxonomies = [] ) {
768 $attribute_names = wc_get_attribute_taxonomy_names();
769
770 foreach ( $attribute_names as $name ) {
771 if ( ! taxonomy_exists( $name ) ) {
772 continue;
773 }
774 $taxonomies[ $name ] = get_taxonomy( $name );
775 }
776
777 return $taxonomies;
778 }
779
780 /**
781 * Add WooCommerce Fields to the Weighting Dashboard.
782 *
783 * @since 3.x
784 *
785 * @param array $fields Current weighting fields.
786 * @param string $post_type Current post type.
787 * @return array New fields.
788 */
789 public function add_product_attributes_to_weighting( $fields, $post_type ) {
790 if ( 'product' === $post_type ) {
791 if ( ! empty( $fields['attributes']['children']['author_name'] ) ) {
792 unset( $fields['attributes']['children']['author_name'] );
793 }
794
795 $sku_key = 'meta._sku.value';
796
797 $fields['attributes']['children'][ $sku_key ] = array(
798 'key' => $sku_key,
799 'label' => __( 'SKU', 'elasticpress' ),
800 );
801
802 $variations_skus_key = 'meta._variations_skus.value';
803
804 $fields['attributes']['children'][ $variations_skus_key ] = array(
805 'key' => $variations_skus_key,
806 'label' => __( 'Variations SKUs', 'elasticpress' ),
807 );
808 }
809 return $fields;
810 }
811
812 /**
813 * Add WooCommerce Fields to the default values of the Weighting Dashboard.
814 *
815 * @since 3.x
816 *
817 * @param array $defaults Default values for the post type.
818 * @param string $post_type Current post type.
819 * @return array
820 */
821 public function add_product_default_post_type_weights( $defaults, $post_type ) {
822 if ( 'product' === $post_type ) {
823 if ( ! empty( $defaults['author_name'] ) ) {
824 unset( $defaults['author_name'] );
825 }
826
827 $defaults['meta._sku.value'] = array(
828 'enabled' => true,
829 'weight' => 1,
830 );
831
832 $defaults['meta._variations_skus.value'] = array(
833 'enabled' => true,
834 'weight' => 1,
835 );
836 }
837 return $defaults;
838 }
839
840 /**
841 * Add WC post type to autosuggest
842 *
843 * @param array $post_types Array of post types (e.g. post, page).
844 * @since 2.6
845 * @return array
846 */
847 public function suggest_wc_add_post_type( $post_types ) {
848 if ( ! in_array( 'product', $post_types, true ) ) {
849 $post_types[] = 'product';
850 }
851
852 return $post_types;
853 }
854
855 /**
856 * Setup all feature filters
857 *
858 * @since 2.1
859 */
860 public function setup() {
861 if ( ! function_exists( 'WC' ) ) {
862 return;
863 }
864
865 add_action( 'ep_formatted_args', [ $this, 'price_filter' ], 10, 3 );
866 add_filter( 'ep_sync_insert_permissions_bypass', [ $this, 'bypass_order_permissions_check' ], 10, 2 );
867 add_filter( 'ep_integrate_search_queries', [ $this, 'blacklist_coupons' ], 10, 2 );
868 add_filter( 'ep_prepare_meta_allowed_protected_keys', [ $this, 'whitelist_meta_keys' ], 10, 2 );
869 add_filter( 'woocommerce_layered_nav_query_post_ids', [ $this, 'convert_post_object_to_id' ], 10, 4 );
870 add_filter( 'woocommerce_unfiltered_product_ids', [ $this, 'convert_post_object_to_id' ], 10, 4 );
871 add_filter( 'ep_sync_taxonomies', [ $this, 'whitelist_taxonomies' ], 10, 2 );
872 add_filter( 'ep_post_sync_args_post_prepare_meta', [ $this, 'add_order_items_search' ], 20, 2 );
873 add_filter( 'ep_pc_skip_post_content_cleanup', [ $this, 'keep_order_fields' ], 20, 2 );
874 add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 );
875 add_action( 'ep_wp_query_search_cached_posts', [ $this, 'disallow_duplicated_query' ], 10, 2 );
876 add_action( 'parse_query', [ $this, 'maybe_hook_woocommerce_search_fields' ], 1 );
877 add_action( 'parse_query', [ $this, 'search_order' ], 11 );
878 add_filter( 'ep_term_suggest_post_type', [ $this, 'suggest_wc_add_post_type' ] );
879 add_filter( 'ep_facet_include_taxonomies', [ $this, 'add_product_attributes' ] );
880 add_filter( 'ep_weighting_fields_for_post_type', [ $this, 'add_product_attributes_to_weighting' ], 10, 2 );
881 add_filter( 'ep_weighting_default_post_type_weights', [ $this, 'add_product_default_post_type_weights' ], 10, 2 );
882 add_filter( 'ep_prepare_meta_data', [ $this, 'add_variations_skus_meta' ], 10, 2 );
883 add_filter( 'request', [ $this, 'admin_product_list_request_query' ], 9 );
884
885 // Custom product ordering
886 add_action( 'ep_admin_notices', [ $this, 'maybe_display_notice_about_product_ordering' ] );
887 add_action( 'woocommerce_after_product_ordering', [ $this, 'action_sync_on_woocommerce_sort_single' ], 10, 2 );
888
889 // Orders Autosuggest feature.
890 if ( $this->is_orders_autosuggest_enabled() ) {
891 $this->orders->setup();
892 }
893
894 // Add WooCommerce Settings for Weight results by date
895 add_action( 'ep_weight_settings_after_search', [ $this, 'add_weight_settings_search' ] );
896 // Modify decaying based on WooCommerce Settings
897 add_filter( 'ep_is_decaying_enabled', [ $this, 'maybe_disable_decaying' ], 10, 3 );
898 }
899
900 /**
901 * Output feature box long
902 *
903 * @since 2.1
904 */
905 public function output_feature_box_long() {
906 ?>
907 <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>
908 <?php
909 }
910
911 /**
912 * Dashboard WooCommerce settings
913 *
914 * @since 4.5.0
915 */
916 public function output_feature_box_settings() {
917 $available = $this->is_orders_autosuggest_available();
918 $enabled = $this->is_orders_autosuggest_enabled();
919 ?>
920 <div class="field">
921 <div class="field-name status"><?php esc_html_e( 'Orders Autosuggest', 'elasticpress' ); ?></div>
922 <div class="input-wrap">
923 <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>
924 <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>
925 <p class="field-description">
926 <?php
927 $epio_autosuggest_kb_link = 'https://elasticpress.zendesk.com/hc/en-us/articles/13374461690381-Configuring-ElasticPress-io-Order-Autosuggest';
928
929 $message = ( $available ) ?
930 /* translators: 1: <a> tag (ElasticPress.io); 2. </a>; 3: <a> tag (KB article); 4. </a>; */
931 __( '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' ) :
932 /* translators: 1: <a> tag (ElasticPress.io); 2. </a>; 3: <a> tag (KB article); 4. </a>; */
933 __( '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' );
934
935 printf(
936 wp_kses( $message, 'ep-html' ),
937 '<a href="https://elasticpress.io/" target="_blank">',
938 '</a>',
939 '<a href="' . esc_url( $epio_autosuggest_kb_link ) . '" target="_blank">',
940 '</a>'
941 );
942 ?>
943 </p>
944 </div>
945 </div>
946 <?php
947 }
948
949 /**
950 * Remove the author_name from search fields.
951 *
952 * @param array $search_fields Array of search fields.
953 * @since 3.0
954 * @return array
955 */
956 public function remove_author( $search_fields ) {
957 foreach ( $search_fields as $field_key => $field ) {
958 if ( 'author_name' === $field ) {
959 unset( $search_fields[ $field_key ] );
960 }
961 }
962
963 return $search_fields;
964 }
965
966 /**
967 * Determine WC feature reqs status
968 *
969 * @since 2.2
970 * @return EP_Feature_Requirements_Status
971 */
972 public function requirements_status() {
973 $status = new FeatureRequirementsStatus( 0 );
974
975 if ( ! class_exists( 'WooCommerce' ) ) {
976 $status->code = 2;
977 $status->message = esc_html__( 'WooCommerce not installed.', 'elasticpress' );
978 }
979
980 return $status;
981 }
982
983 /**
984 * Modifies main query to allow filtering by price with WooCommerce "Filter by price" widget.
985 *
986 * @param array $args ES args
987 * @param array $query_args WP_Query args
988 * @param WP_Query $query WP_Query object
989 * @since 3.2
990 * @return array
991 */
992 public function price_filter( $args, $query_args, $query ) {
993 // Only can use widget on main query
994 if ( ! $query->is_main_query() ) {
995 return $args;
996 }
997
998 // Only can use widget on shop, product taxonomy, or search
999 if ( ! is_shop() && ! is_product_taxonomy() && ! is_search() ) {
1000 return $args;
1001 }
1002
1003 // phpcs:disable WordPress.Security.NonceVerification
1004 if ( empty( $_GET['min_price'] ) && empty( $_GET['max_price'] ) ) {
1005 return $args;
1006 }
1007
1008 $min_price = ! empty( $_GET['min_price'] ) ? sanitize_text_field( wp_unslash( $_GET['min_price'] ) ) : null;
1009 $max_price = ! empty( $_GET['max_price'] ) ? sanitize_text_field( wp_unslash( $_GET['max_price'] ) ) : null;
1010 // phpcs:enable WordPress.Security.NonceVerification
1011
1012 if ( $query->is_search() ) {
1013 /**
1014 * This logic is iffy but the WC price filter widget is not intended for use with search anyway
1015 */
1016 $old_query = $args['query']['bool'];
1017 unset( $args['query']['bool']['should'] );
1018
1019 if ( ! empty( $min_price ) ) {
1020 $args['query']['bool']['must'][0]['range']['meta._price.long']['gte'] = $min_price;
1021 }
1022
1023 if ( ! empty( $max_price ) ) {
1024 $args['query']['bool']['must'][0]['range']['meta._price.long']['lte'] = $max_price;
1025 }
1026
1027 $args['query']['bool']['must'][0]['range']['meta._price.long']['boost'] = 2.0;
1028 $args['query']['bool']['must'][1]['bool'] = $old_query;
1029 } else {
1030 unset( $args['query']['match_all'] );
1031
1032 $args['query']['range']['meta._price.long']['gte'] = ! empty( $min_price ) ? $min_price : 0;
1033
1034 if ( ! empty( $min_price ) ) {
1035 $args['query']['range']['meta._price.long']['gte'] = $min_price;
1036 }
1037
1038 if ( ! empty( $max_price ) ) {
1039 $args['query']['range']['meta._price.long']['lte'] = $max_price;
1040 }
1041
1042 $args['query']['range']['meta._price.long']['boost'] = 2.0;
1043 }
1044
1045 return $args;
1046 }
1047
1048 /**
1049 * Prevent order fields from being removed.
1050 *
1051 * When Protected Content is enabled, all posts with password have their content removed.
1052 * This can't happen for orders, as the order key is added in that field.
1053 *
1054 * @see https://github.com/10up/ElasticPress/issues/2726
1055 *
1056 * @since 4.2.0
1057 * @param bool $skip Whether the password protected content should have their content, and meta removed
1058 * @param array $post_args Post arguments
1059 * @return bool
1060 */
1061 public function keep_order_fields( $skip, $post_args ) {
1062 $searchable_post_types = $this->get_admin_searchable_post_types();
1063
1064 if ( in_array( $post_args['post_type'], $searchable_post_types, true ) ) {
1065 return true;
1066 }
1067
1068 return $skip;
1069 }
1070
1071 /**
1072 * Add a new `_variations_skus` meta field to the product to be indexed in Elasticsearch.
1073 *
1074 * @since 4.2.0
1075 * @param array $post_meta Post meta
1076 * @param WP_Post $post Post object
1077 * @return array
1078 */
1079 public function add_variations_skus_meta( $post_meta, $post ) {
1080 if ( 'product' !== $post->post_type ) {
1081 return $post_meta;
1082 }
1083
1084 $product = wc_get_product( $post );
1085 if ( ! $product ) {
1086 return $post_meta;
1087 }
1088
1089 $variations_ids = $product->get_children();
1090
1091 $post_meta['_variations_skus'] = array_reduce(
1092 $variations_ids,
1093 function ( $variations_skus, $current_id ) {
1094 $variation = wc_get_product( $current_id );
1095 if ( ! $variation || ! $variation->exists() ) {
1096 return $variations_skus;
1097 }
1098 $variation_sku = $variation->get_sku();
1099 if ( ! $variation_sku ) {
1100 return $variations_skus;
1101 }
1102 $variations_skus[] = $variation_sku;
1103 return $variations_skus;
1104 },
1105 []
1106 );
1107
1108 return $post_meta;
1109 }
1110
1111 /**
1112 * Integrate ElasticPress with the WooCommerce Admin Product List.
1113 *
1114 * WooCommerce uses its `WC_Admin_List_Table_Products` class to control that screen. This
1115 * function adds all necessary hooks to bypass the default behavior and integrate with ElasticPress.
1116 * By default, WC runs a SQL query to get the Product IDs that match the list criteria and passes
1117 * that list of IDs to the main WP_Query. This integration changes that process to a single query, run
1118 * by ElasticPress.
1119 *
1120 * @since 4.2.0
1121 * @param array $query_vars Query vars.
1122 * @return array
1123 */
1124 public function admin_product_list_request_query( $query_vars ) {
1125 global $typenow, $wc_list_table;
1126
1127 // Return if not in the correct screen.
1128 if ( ! is_a( $wc_list_table, 'WC_Admin_List_Table_Products' ) || 'product' !== $typenow ) {
1129 return $query_vars;
1130 }
1131
1132 // Return if admin WP_Query integration is not turned on, i.e., Protect Content is not enabled.
1133 if ( ! has_filter( 'ep_admin_wp_query_integration', '__return_true' ) ) {
1134 return $query_vars;
1135 }
1136
1137 /**
1138 * Filter to skip integration with WooCommerce Admin Product List.
1139 *
1140 * @hook ep_woocommerce_integrate_admin_products_list
1141 * @since 4.2.0
1142 * @param {bool} $integrate True to integrate, false to preserve original behavior. Defaults to true.
1143 * @param {array} $query_vars Query vars.
1144 * @return {bool} New integrate value
1145 */
1146 if ( ! apply_filters( 'ep_woocommerce_integrate_admin_products_list', true, $query_vars ) ) {
1147 return $query_vars;
1148 }
1149
1150 add_action( 'pre_get_posts', [ $this, 'translate_args_admin_products_list' ], 12 );
1151
1152 // This short-circuits WooCommerce search for product IDs.
1153 add_filter( 'woocommerce_product_pre_search_products', '__return_empty_array' );
1154
1155 return $query_vars;
1156 }
1157
1158 /**
1159 * Apply the necessary changes to WP_Query in WooCommerce Admin Product List.
1160 *
1161 * @param WP_Query $query The WP Query being executed.
1162 */
1163 public function translate_args_admin_products_list( $query ) {
1164 // The `translate_args()` method sets it to `true` if we should integrate it.
1165 if ( ! $query->get( 'ep_integrate', false ) ) {
1166 return;
1167 }
1168
1169 // WooCommerce unsets the search term right after using it to fetch product IDs. Here we add it back.
1170 $search_term = ! empty( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
1171 if ( ! empty( $search_term ) ) {
1172 $query->set( 's', sanitize_text_field( $search_term ) ); // phpcs:ignore WordPress.Security.NonceVerification
1173
1174 /**
1175 * Filter the fields used in WooCommerce Admin Product Search.
1176 *
1177 * ```
1178 * add_filter(
1179 * 'ep_woocommerce_admin_products_list_search_fields',
1180 * function ( $wc_admin_search_fields ) {
1181 * $wc_admin_search_fields['meta'][] = 'custom_field';
1182 * return $wc_admin_search_fields;
1183 * }
1184 * );
1185 * ```
1186 *
1187 * @hook ep_woocommerce_admin_products_list_search_fields
1188 * @since 4.2.0
1189 * @param {array} $wc_admin_search_fields Fields to be used in the WooCommerce Admin Product Search
1190 * @return {array} New fields
1191 */
1192 $search_fields = apply_filters(
1193 'ep_woocommerce_admin_products_list_search_fields',
1194 [
1195 'post_title',
1196 'post_content',
1197 'post_excerpt',
1198 'meta' => [
1199 '_sku',
1200 '_variations_skus',
1201 ],
1202 ]
1203 );
1204
1205 $query->set( 'search_fields', $search_fields );
1206 }
1207
1208 // 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()`.
1209 $product_type_query = $query->get( 'product_type', '' );
1210 $product_type_url = ! empty( $_GET['product_type'] ) ? sanitize_text_field( wp_unslash( $_GET['product_type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
1211 $allowed_prod_types = [ 'virtual', 'downloadable' ];
1212 if ( empty( $product_type_query ) && ! empty( $product_type_url ) && in_array( $product_type_url, $allowed_prod_types, true ) ) {
1213 $meta_query = $query->get( 'meta_query', [] );
1214 $meta_query[] = [
1215 'key' => "_{$product_type_url}",
1216 'value' => 'yes',
1217 ];
1218 $query->set( 'meta_query', $meta_query );
1219 }
1220
1221 // Sets the meta query for `stock_status` if needed.
1222 $stock_status_query = $query->get( 'stock_status', '' );
1223 $stock_status_url = ! empty( $_GET['stock_status'] ) ? sanitize_text_field( wp_unslash( $_GET['stock_status'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
1224 $allowed_stock_status = [ 'instock', 'outofstock', 'onbackorder' ];
1225 if ( empty( $stock_status_query ) && ! empty( $stock_status_url ) && in_array( $stock_status_url, $allowed_stock_status, true ) ) {
1226 $meta_query = $query->get( 'meta_query', [] );
1227 $meta_query[] = [
1228 'key' => '_stock_status',
1229 'value' => $stock_status_url,
1230 ];
1231 $query->set( 'meta_query', $meta_query );
1232 }
1233 }
1234
1235 /**
1236 * Determines whether or not ES should be integrating with the provided query
1237 *
1238 * @param \WP_Query $query Query we might integrate with
1239 *
1240 * @return bool
1241 */
1242 protected function should_integrate_with_query( $query ) {
1243 // Lets make sure this doesn't interfere with the CLI
1244 if ( defined( 'WP_CLI' ) && WP_CLI ) {
1245 return false;
1246 }
1247
1248 if ( defined( 'WC_API_REQUEST' ) && WC_API_REQUEST ) {
1249 return false;
1250 }
1251
1252 if ( isset( $query->query_vars['ep_integrate'] ) && ! filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN ) ) {
1253 return false;
1254 }
1255
1256 /**
1257 * Filter to skip WP Query integration
1258 *
1259 * @hook ep_skip_query_integration
1260 * @param {bool} $skip True to skip
1261 * @param {WP_Query} $query WP Query to evaluate
1262 * @return {bool} New skip value
1263 */
1264 if ( apply_filters( 'ep_skip_query_integration', false, $query ) ) {
1265 return false;
1266 }
1267
1268 if ( ! Utils\is_integrated_request( $this->slug ) ) {
1269 return false;
1270 }
1271
1272 /**
1273 * Do nothing for single product queries
1274 */
1275 $product_name = $query->get( 'product', false );
1276 if ( ! empty( $product_name ) || $query->is_single() ) {
1277 return false;
1278 }
1279
1280 /**
1281 * ElasticPress does not yet support post_parent queries
1282 */
1283 $post_parent = $query->get( 'post_parent', false );
1284 if ( ! empty( $post_parent ) ) {
1285 return false;
1286 }
1287
1288 /**
1289 * If this is just a preview, let's not use Elasticsearch.
1290 */
1291 if ( $query->get( 'preview', false ) ) {
1292 return false;
1293 }
1294
1295 return true;
1296 }
1297
1298 /**
1299 * Depending on the number of products display an admin notice in the custom sort screen for WooCommerce Products
1300 *
1301 * @since 4.4.0
1302 * @param array $notices Current ElasticPress admin notices
1303 * @return array
1304 */
1305 public function maybe_display_notice_about_product_ordering( $notices ) {
1306 global $pagenow, $wp_query;
1307
1308 /**
1309 * Make sure we're on edit.php in admin dashboard.
1310 */
1311 if ( ! is_admin() || 'edit.php' !== $pagenow || empty( $wp_query->query['orderby'] ) || 'menu_order title' !== $wp_query->query['orderby'] ) {
1312 return $notices;
1313 }
1314
1315 $documents_per_page_sync = IndexHelper::factory()->get_index_default_per_page();
1316 if ( $documents_per_page_sync >= $wp_query->found_posts ) {
1317 return $notices;
1318 }
1319
1320 $notices['woocommerce_custom_sort'] = [
1321 'html' => sprintf(
1322 /* translators: Sync Page URL */
1323 __( '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' ),
1324 Utils\get_sync_url()
1325 ),
1326 'type' => 'warning',
1327 'dismiss' => true,
1328 ];
1329
1330 return $notices;
1331 }
1332
1333 /**
1334 * Conditionally resync products after applying a custom order.
1335 *
1336 * @since 4.4.0
1337 * @param int $sorting_id ID of post dragged and dropped
1338 * @param array $menu_orders Post IDs and their new menu_order value
1339 */
1340 public function action_sync_on_woocommerce_sort_single( $sorting_id, $menu_orders ) {
1341
1342 $documents_per_page_sync = IndexHelper::factory()->get_index_default_per_page();
1343 if ( $documents_per_page_sync < count( $menu_orders ) ) {
1344 return;
1345 }
1346
1347 $sync_manager = Indexables::factory()->get( 'post' )->sync_manager;
1348 foreach ( $menu_orders as $post_id => $order ) {
1349 $sync_manager->add_to_queue( $post_id );
1350 }
1351 }
1352
1353 /**
1354 * Whether orders autosuggest is available or not
1355 *
1356 * @since 4.5.0
1357 * @return boolean
1358 */
1359 public function is_orders_autosuggest_available() : bool {
1360 /**
1361 * Whether the autosuggest feature is available for non
1362 * ElasticPress.io customers.
1363 *
1364 * @since 4.5.0
1365 * @hook ep_woocommerce_orders_autosuggest_available
1366 * @param {boolean} $available Whether the feature is available.
1367 */
1368 return apply_filters( 'ep_woocommerce_orders_autosuggest_available', Utils\is_epio() );
1369 }
1370
1371 /**
1372 * Whether orders autosuggest is enabled or not
1373 *
1374 * @since 4.5.0
1375 * @return boolean
1376 */
1377 public function is_orders_autosuggest_enabled() : bool {
1378 return $this->is_orders_autosuggest_available() && '1' === $this->get_setting( 'orders' );
1379 }
1380
1381 /**
1382 * Add weight by date settings related to WooCommerce
1383 *
1384 * @since 4.6.0
1385 * @param array $settings Current settings.
1386 */
1387 public function add_weight_settings_search( $settings ) {
1388 ?>
1389 <label><input name="settings[decaying_enabled]" type="radio" <?php checked( $settings['decaying_enabled'], 'disabled_only_products' ); ?> value="disabled_only_products"><?php esc_html_e( 'Disabled for product only queries', 'elasticpress' ); ?></label><br>
1390 <label><input name="settings[decaying_enabled]" type="radio" <?php checked( $settings['decaying_enabled'], 'disabled_includes_products' ); ?> value="disabled_includes_products"><?php esc_html_e( 'Disabled for any query that includes products', 'elasticpress' ); ?></label>
1391 <?php
1392 }
1393
1394 /**
1395 * Conditionally disable decaying by date based on WooCommerce Decay settings.
1396 *
1397 * @since 4.6.0
1398 * @param bool $is_decaying_enabled Whether decay by date is enabled or not
1399 * @param array $settings Settings
1400 * @param array $args WP_Query args
1401 * @return bool
1402 */
1403 public function maybe_disable_decaying( $is_decaying_enabled, $settings, $args ) {
1404 if ( ! in_array( $settings['decaying_enabled'], [ 'disabled_only_products', 'disabled_includes_products' ], true ) ) {
1405 return $is_decaying_enabled;
1406 }
1407
1408 if ( ! isset( $args['post_type'] ) || ! in_array( 'product', (array) $args['post_type'], true ) ) {
1409 return $is_decaying_enabled;
1410 }
1411
1412 $post_types = (array) $args['post_type'];
1413
1414 if ( 'disabled_only_products' === $settings['decaying_enabled'] && count( $post_types ) > 1 ) {
1415 return $is_decaying_enabled;
1416 }
1417
1418 if ( 'disabled_includes_products' === $settings['decaying_enabled'] && ! in_array( 'product', $post_types, true ) ) {
1419 return $is_decaying_enabled;
1420 }
1421
1422 return false;
1423 }
1424
1425 }
1426