<?php
/**
* WooCommerce Orders
*
* @since 4.7.0
* @package elasticpress
*/
namespace ElasticPress\Feature\WooCommerce;
use ElasticPress\Indexables;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* WooCommerce Orders
*/
class Orders {
/**
* WooCommerce feature object instance
*
* @var WooCommerce
*/
protected $woocommerce;
/**
* Class constructor
*
* @param WooCommerce $woocommerce WooCommerce feature object instance
*/
public function __construct( WooCommerce $woocommerce ) {
$this->woocommerce = $woocommerce;
}
/**
* Setup order related hooks
*/
public function setup() {
add_filter( 'ep_sync_insert_permissions_bypass', [ $this, 'bypass_order_permissions_check' ], 10, 2 );
add_filter( 'ep_prepare_meta_allowed_protected_keys', [ $this, 'allow_meta_keys' ], 10, 2 );
add_filter( 'ep_post_sync_args_post_prepare_meta', [ $this, 'add_order_items_search' ], 20, 2 );
add_filter( 'ep_pc_skip_post_content_cleanup', [ $this, 'keep_order_fields' ], 20, 2 );
add_action( 'parse_query', [ $this, 'maybe_hook_woocommerce_search_fields' ], 1 );
add_action( 'parse_query', [ $this, 'search_order' ], 11 );
add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 );
add_filter( 'ep_admin_notices', [ $this, 'hpos_compatibility_notice' ] );
}
/**
* Unsetup order related hooks
*
* @since 5.0.0
*/
public function tear_down() {
remove_filter( 'ep_sync_insert_permissions_bypass', [ $this, 'bypass_order_permissions_check' ] );
remove_filter( 'ep_prepare_meta_allowed_protected_keys', [ $this, 'allow_meta_keys' ] );
remove_filter( 'ep_post_sync_args_post_prepare_meta', [ $this, 'add_order_items_search' ], 20 );
remove_filter( 'ep_pc_skip_post_content_cleanup', [ $this, 'keep_order_fields' ], 20 );
remove_action( 'parse_query', [ $this, 'maybe_hook_woocommerce_search_fields' ], 1 );
remove_action( 'parse_query', [ $this, 'search_order' ], 11 );
remove_action( 'pre_get_posts', [ $this, 'translate_args' ], 11 );
}
/**
* Allow order creations on the front end to get synced
*
* @param bool $override Original order perms check value
* @param int $post_id Post ID
* @return bool
*/
public function bypass_order_permissions_check( $override, $post_id ) {
$searchable_post_types = $this->get_admin_searchable_post_types();
if ( in_array( get_post_type( $post_id ), $searchable_post_types, true ) ) {
return true;
}
return $override;
}
/**
* Returns the WooCommerce-oriented post types in admin that EP will search
*
* @return array
*/
public function get_admin_searchable_post_types() {
$searchable_post_types = array( 'shop_order' );
/**
* Filter admin searchable WooCommerce post types
*
* @hook ep_woocommerce_admin_searchable_post_types
* @since 4.4.0
* @param {array} $post_types Post types
* @return {array} New post types
*/
return apply_filters( 'ep_woocommerce_admin_searchable_post_types', $searchable_post_types );
}
/**
* Index WooCommerce orders meta fields
*
* @param array $meta Existing post meta
* @param \WP_Post $post Post object.
* @return array
*/
public function allow_meta_keys( $meta, $post ) {
if ( ! in_array( $post->post_type, $this->get_supported_post_types(), true ) ) {
return $meta;
}
return array_unique(
array_merge(
$meta,
array(
'_customer_user',
'_order_key',
'_billing_company',
'_billing_address_1',
'_billing_address_2',
'_billing_city',
'_billing_postcode',
'_billing_country',
'_billing_state',
'_billing_email',
'_billing_phone',
'_shipping_address_1',
'_shipping_address_2',
'_shipping_city',
'_shipping_postcode',
'_shipping_country',
'_shipping_state',
'_billing_last_name',
'_billing_first_name',
'_shipping_first_name',
'_shipping_last_name',
'_variations_skus',
)
)
);
}
/**
* Add order items as a searchable string.
*
* This mimics how WooCommerce currently does in the order_itemmeta
* table. They combine the titles of the products and put them in a
* meta field called "Items".
*
* @param array $post_args Post arguments
* @param string|int $post_id Post id
*
* @return array
*/
public function add_order_items_search( $post_args, $post_id ) {
$order = wc_get_order( $post_id );
if ( ! $order ) {
return $post_args;
}
$searchable_post_types = $this->get_admin_searchable_post_types();
// Make sure it is only WooCommerce orders we touch.
if ( ! in_array( $post_args['post_type'], $searchable_post_types, true ) ) {
return $post_args;
}
$post_indexable = Indexables::factory()->get( 'post' );
// Get order items.
$item_meta = [];
foreach ( $order->get_items() as $delta => $product_item ) {
// WooCommerce 3.x uses WC_Order_Item_Product instance while 2.x an array
if ( is_object( $product_item ) && method_exists( $product_item, 'get_name' ) ) {
$item_meta['_items'][] = $product_item->get_name( 'edit' );
} elseif ( is_array( $product_item ) && isset( $product_item['name'] ) ) {
$item_meta['_items'][] = $product_item['name'];
}
}
// Prepare order items.
$item_meta['_items'] = empty( $item_meta['_items'] ) ? '' : implode( '|', $item_meta['_items'] );
$post_args['meta'] = array_merge( $post_args['meta'], $post_indexable->prepare_meta_types( $item_meta ) );
return $post_args;
}
/**
* Prevent order fields from being removed.
*
* When Protected Content is enabled, all posts with password have their content removed.
* This can't happen for orders, as the order key is added in that field.
*
* @see https://github.com/10up/ElasticPress/issues/2726
*
* @param bool $skip Whether the password protected content should have their content, and meta removed
* @param array $post_args Post arguments
* @return bool
*/
public function keep_order_fields( $skip, $post_args ) {
$searchable_post_types = $this->get_admin_searchable_post_types();
if ( in_array( $post_args['post_type'], $searchable_post_types, true ) ) {
return true;
}
return $skip;
}
/**
* Sets WooCommerce meta search fields to an empty array if we are integrating the main query with ElasticSearch
*
* WooCommerce calls this action as part of its own callback on parse_query. We add this filter only if the query
* is integrated with ElasticSearch.
* If we were to always return array() on this filter, we'd break admin searches when WooCommerce module is activated
* without the Protected Content Module
*
* @param \WP_Query $query Current query
*/
public function maybe_hook_woocommerce_search_fields( $query ) {
global $pagenow, $wp, $wc_list_table;
if ( ! $this->woocommerce->should_integrate_with_query( $query ) ) {
return;
}
/**
* Determines actions to be applied, or removed, if doing a WooCommerce serarch
*
* @hook ep_woocommerce_hook_search_fields
* @since 4.4.0
*/
do_action( 'ep_woocommerce_hook_search_fields' );
if ( 'edit.php' !== $pagenow || empty( $wp->query_vars['s'] ) || 'shop_order' !== $wp->query_vars['post_type'] || ! isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return;
}
remove_action( 'parse_query', [ $wc_list_table, 'search_custom_fields' ] );
}
/**
* Enhance WooCommerce search order by order id, email, phone number, name, etc..
* What this function does:
* 1. Reverse the woocommerce shop_order_search_custom_fields query
* 2. If the search key is integer and it is an Order Id, just query with post__in
* 3. If the search key is integer but not an order id ( might be phone number ), use ES to find it
*
* @param WP_Query $wp WP Query
*/
public function search_order( $wp ) {
global $pagenow;
if ( ! $this->woocommerce->should_integrate_with_query( $wp ) ) {
return;
}
$searchable_post_types = $this->get_admin_searchable_post_types();
if ( 'edit.php' !== $pagenow || empty( $wp->query_vars['post_type'] ) || ! in_array( $wp->query_vars['post_type'], $searchable_post_types, true ) ||
( empty( $wp->query_vars['s'] ) && empty( $wp->query_vars['shop_order_search'] ) ) ) {
return;
}
// phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
if ( isset( $_GET['s'] ) ) {
$search_key_safe = str_replace( array( 'Order #', '#' ), '', wc_clean( $_GET['s'] ) );
unset( $wp->query_vars['post__in'] );
$wp->query_vars['s'] = $search_key_safe;
}
// phpcs:enable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
}
/**
* Determines whether or not ES should be integrating with the provided query
*
* @param \WP_Query $query Query we might integrate with
* @return bool
*/
public function should_integrate_with_query( \WP_Query $query ) : bool {
/**
* Check the post type
*/
$supported_post_types = $this->get_supported_post_types( $query );
$post_type = $query->get( 'post_type', false );
if ( ! empty( $post_type ) &&
( in_array( $post_type, $supported_post_types, true ) ||
( is_array( $post_type ) && ! array_diff( $post_type, $supported_post_types ) ) )
) {
return true;
}
return false;
}
/**
* Get the supported post types for Order related queries
*
* @return array
*/
public function get_supported_post_types() : array {
$post_types = [ 'shop_order', 'shop_order_refund' ];
/**
* DEPRECATED. Expands or contracts the post_types eligible for indexing.
*
* @hook ep_woocommerce_default_supported_post_types
* @since 4.4.0
* @param {array} $post_types Post types
* @return {array} New post types
*/
$supported_post_types = apply_filters_deprecated(
'ep_woocommerce_default_supported_post_types',
[ $post_types ],
'4.7.0',
'ep_woocommerce_orders_supported_post_types'
);
/**
* Expands or contracts the post_types related to orders eligible for indexing.
*
* @hook ep_woocommerce_orders_supported_post_types
* @since 4.7.0
* @param {array} $supported_post_types Post types
* @return {array} New post types
*/
$supported_post_types = apply_filters( 'ep_woocommerce_orders_supported_post_types', $supported_post_types );
$supported_post_types = array_intersect(
$supported_post_types,
Indexables::factory()->get( 'post' )->get_indexable_post_types()
);
return $supported_post_types;
}
/**
* Display a notice if WooCommerce Orders are not compatible with ElasticPress
*
* If the user has WooCommerce, Protected Content, and HPOS enabled, orders will not go through ElasticPress.
*
* @param array $notices Current EP notices
* @return array
*/
public function hpos_compatibility_notice( array $notices ) : array {
$current_screen = \get_current_screen();
if ( empty( $current_screen->id ) || 'woocommerce_page_wc-orders' !== $current_screen->id ) {
return $notices;
}
if ( \ElasticPress\Utils\get_option( 'ep_hide_wc_orders_incompatible_notice' ) ) {
return $notices;
}
$protected_content = \ElasticPress\Features::factory()->get_registered_feature( 'protected_content' );
if ( ! $protected_content->is_active() ) {
return $notices;
}
if (
! class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
|| ! method_exists( '\Automattic\WooCommerce\Utilities\OrderUtil', 'custom_orders_table_usage_is_enabled' ) ) {
return $notices;
}
if ( ! \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) {
return $notices;
}
$notices['wc_orders_incompatible'] = [
'html' => esc_html__( "Although the WooCommerce and Protected Content features are enabled, ElasticPress will not integrate with the WooCommerce Orders list if WooCommerce's High-performance order storage is enabled.", 'elasticpress' ),
'type' => 'warning',
'dismiss' => true,
];
return $notices;
}
/**
* If the query has a search term, add the order fields that need to be searched.
*
* @param \WP_Query $query The WP_Query
* @return \WP_Query
*/
protected function maybe_set_search_fields( \WP_Query $query ) {
$search_term = $this->woocommerce->get_search_term( $query );
if ( empty( $search_term ) ) {
return $query;
}
$searchable_post_types = $this->get_admin_searchable_post_types();
$post_type = $query->get( 'post_type', false );
if ( ! in_array( $post_type, $searchable_post_types, true ) ) {
return $query;
}
$default_search_fields = array( 'post_title', 'post_content', 'post_excerpt' );
if ( ctype_digit( $search_term ) ) {
$default_search_fields[] = 'ID';
}
$search_fields = $query->get( 'search_fields', $default_search_fields );
$search_fields['meta'] = array_map(
'wc_clean',
/**
* Filter shop order meta fields to search for WooCommerce
*
* @hook shop_order_search_fields
* @param {array} $fields Shop order fields
* @return {array} New fields
*/
apply_filters(
'shop_order_search_fields',
array(
'_order_key',
'_billing_company',
'_billing_address_1',
'_billing_address_2',
'_billing_city',
'_billing_postcode',
'_billing_country',
'_billing_state',
'_billing_email',
'_billing_phone',
'_shipping_address_1',
'_shipping_address_2',
'_shipping_city',
'_shipping_postcode',
'_shipping_country',
'_shipping_state',
'_billing_last_name',
'_billing_first_name',
'_shipping_first_name',
'_shipping_last_name',
'_items',
)
)
);
$query->set(
'search_fields',
/**
* Filter all the shop order fields to search for WooCommerce
*
* @hook ep_woocommerce_shop_order_search_fields
* @since 4.0.0
* @param {array} $fields Shop order fields
* @param {WP_Query} $query WP Query
* @return {array} New fields
*/
apply_filters( 'ep_woocommerce_shop_order_search_fields', $search_fields, $query )
);
}
/**
* Translate args to ElasticPress compat format. This is the meat of what the feature does
*
* @param \WP_Query $query WP Query
*/
public function translate_args( $query ) {
if ( ! $this->woocommerce->should_integrate_with_query( $query ) ) {
return;
}
if ( ! $this->should_integrate_with_query( $query ) ) {
return;
}
$query->set( 'ep_integrate', true );
/**
* Make sure filters are suppressed
*/
$query->query['suppress_filters'] = false;
$query->set( 'suppress_filters', false );
$this->maybe_set_search_fields( $query );
}
/**
* Handle calls to OrdersAutosuggest methods
*
* @param string $method_name The method name
* @param array $arguments Array of arguments
*/
public function __call( $method_name, $arguments ) {
$orders_autosuggest_methods = [
'after_update_feature',
'check_token_permission',
'enqueue_admin_assets',
'epio_delete_search_template',
'epio_get_search_template',
'epio_save_search_template',
'filter_term_suggest',
'get_args_schema',
'get_search_endpoint',
'get_search_template',
'get_template_endpoint',
'get_token',
'get_token_endpoint',
'intercept_search_request',
'is_integrated_request',
'post_statuses',
'post_types',
'mapping',
'maybe_query_password_protected_posts',
'maybe_set_posts_where',
'refresh_token',
'rest_api_init',
'set_search_fields',
];
if ( in_array( $method_name, $orders_autosuggest_methods, true ) ) {
_deprecated_function(
"\ElasticPress\Feature\WooCommerce\WooCommerce\Orders::{$method_name}", // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
'4.7.0',
"\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders_autosuggest->{$method_name}()" // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
if ( $this->woocommerce->orders_autosuggest->is_enabled() && method_exists( $this->woocommerce->orders_autosuggest, $method_name ) ) {
call_user_func_array( [ $this->woocommerce->orders_autosuggest, $method_name ], $arguments );
}
}
}
}