From 6e6f5ceed767cb5a6003009910cd88d2e0ff18bc Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Mon, 19 Jun 2023 17:16:05 -0300 Subject: [PATCH 01/11] Initial pass of WooCommerce code split --- .../classes/Feature/WooCommerce/Orders.php | 653 +++------- .../Feature/WooCommerce/OrdersAutosuggest.php | 605 +++++++++ .../classes/Feature/WooCommerce/Products.php | 585 +++++++++ .../Feature/WooCommerce/WooCommerce.php | 1119 +++++------------ 4 files changed, 1680 insertions(+), 1282 deletions(-) create mode 100644 includes/classes/Feature/WooCommerce/OrdersAutosuggest.php create mode 100644 includes/classes/Feature/WooCommerce/Products.php diff --git a/includes/classes/Feature/WooCommerce/Orders.php b/includes/classes/Feature/WooCommerce/Orders.php index 461a1b51d..d4cd0bf94 100644 --- a/includes/classes/Feature/WooCommerce/Orders.php +++ b/includes/classes/Feature/WooCommerce/Orders.php @@ -1,605 +1,248 @@ index = Indexables::factory()->get( 'post' )->get_index_name(); - } - - /** - * Setup feature functionality. - * - * @return void - */ - public function setup() { - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); - add_filter( 'ep_after_update_feature', [ $this, 'after_update_feature' ], 10, 3 ); - add_filter( 'ep_after_sync_index', [ $this, 'epio_save_search_template' ] ); - add_filter( 'ep_saved_weighting_configuration', [ $this, 'epio_save_search_template' ] ); - add_filter( 'ep_indexable_post_status', [ $this, 'post_statuses' ] ); - add_filter( 'ep_indexable_post_types', [ $this, 'post_types' ] ); - add_action( 'rest_api_init', [ $this, 'rest_api_init' ] ); - add_filter( 'ep_post_sync_args', [ $this, 'filter_term_suggest' ], 10 ); - add_filter( 'ep_post_mapping', [ $this, 'mapping' ] ); - add_action( 'ep_woocommerce_shop_order_search_fields', [ $this, 'set_search_fields' ], 10, 2 ); - add_filter( 'ep_index_posts_args', [ $this, 'maybe_query_password_protected_posts' ] ); - add_filter( 'posts_where', [ $this, 'maybe_set_posts_where' ], 10, 2 ); - } + protected $woocommerce; /** - * Get the endpoint for WooCommerce Orders search. + * Class constructor * - * @return string WooCommerce orders search endpoint. + * @param WooCommerce $woocommerce WooCommerce feature object instance */ - public function get_search_endpoint() { - /** - * Filters the WooCommerce Orders search endpoint. - * - * @since 4.5.0 - * @hook ep_woocommerce_order_search_endpoint - * @param {string} $endpoint Endpoint path. - * @param {string} $index Elasticsearch index. - */ - return apply_filters( 'ep_woocommerce_order_search_endpoint', "api/v1/search/orders/{$this->index}", $this->index ); + public function __construct( WooCommerce $woocommerce ) { + $this->woocommerce = $woocommerce; } /** - * Get the endpoint for the WooCommerce Orders search template. - * - * @return string WooCommerce Orders search template endpoint. + * Setup order related hooks */ - public function get_template_endpoint() { - /** - * Filters the WooCommerce Orders search template API endpoint. - * - * @since 4.5.0 - * @hook ep_woocommerce_order_search_template_endpoint - * @param {string} $endpoint Endpoint path. - * @param {string} $index Elasticsearch index. - * @returns {string} Search template API endpoint. - */ - return apply_filters( 'ep_woocommerce_order_search_template_endpoint', "api/v1/search/orders/{$this->index}/template", $this->index ); - } - - /** - * Get the endpoint for temporary tokens. - * - * @return string Temporary token endpoint. - */ - public function get_token_endpoint() { - /** - * Filters the temporary token API endpoint. - * - * @since 4.5.0 - * @hook ep_token_endpoint - * @param {string} $endpoint Endpoint path. - * @returns {string} Token API endpoint. - */ - return apply_filters( 'ep_token_endpoint', 'api/v1/token' ); - } - - /** - * Registers the API endpoint to get a token. - * - * @return void - */ - public function rest_api_init() { - register_rest_route( - 'elasticpress/v1', - 'token', - [ - [ - 'callback' => [ $this, 'get_token' ], - 'permission_callback' => [ $this, 'check_token_permission' ], - 'methods' => 'GET', - ], - [ - 'callback' => [ $this, 'refresh_token' ], - 'permission_callback' => [ $this, 'check_token_permission' ], - 'methods' => 'POST', - ], - ] - ); - } - - /** - * Enqueue admin assets. - * - * @param string $hook_suffix The current admin page. - */ - public function enqueue_admin_assets( $hook_suffix ) { - if ( 'edit.php' !== $hook_suffix ) { - return; - } - - if ( ! isset( $_GET['post_type'] ) || 'shop_order' !== $_GET['post_type'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - return; - } - - wp_enqueue_style( - 'elasticpress-woocommerce-order-search', - EP_URL . 'dist/css/woocommerce-order-search-styles.css', - Utils\get_asset_info( 'woocommerce-order-search-styles', 'dependencies' ), - Utils\get_asset_info( 'woocommerce-order-search-styles', 'version' ) - ); - - wp_enqueue_script( - 'elasticpress-woocommerce-order-search', - EP_URL . 'dist/js/woocommerce-order-search-script.js', - Utils\get_asset_info( 'woocommerce-order-search-script', 'dependencies' ), - Utils\get_asset_info( 'woocommerce-order-search-script', 'version' ), - true - ); - - wp_set_script_translations( 'elasticpress-woocommerce-order-search', 'elasticpress' ); - - $api_endpoint = $this->get_search_endpoint(); - $api_host = Utils\get_host(); - - wp_localize_script( - 'elasticpress-woocommerce-order-search', - 'epWooCommerceOrderSearch', - array( - 'adminUrl' => admin_url( 'post.php' ), - 'apiEndpoint' => $api_endpoint, - 'apiHost' => ( 0 !== strpos( $api_endpoint, 'http' ) ) ? trailingslashit( esc_url_raw( $api_host ) ) : '', - 'argsSchema' => $this->get_args_schema(), - 'credentialsApiUrl' => rest_url( 'elasticpress/v1/token' ), - 'credentialsNonce' => wp_create_nonce( 'wp_rest' ), - 'dateFormat' => wc_date_format(), - 'statusLabels' => wc_get_order_statuses(), - 'timeFormat' => wc_time_format(), - 'requestIdBase' => Utils\get_request_id_base(), - ) - ); + 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' ] ); + add_filter( 'ep_post_sync_args_post_prepare_meta', [ $this, 'add_order_items_search' ], 20, 2 ); + add_action( 'parse_query', [ $this, 'maybe_hook_woocommerce_search_fields' ], 1 ); + add_action( 'parse_query', [ $this, 'search_order' ], 11 ); } /** - * Save or delete the search template on ElasticPress.io based on whether - * the WooCommerce feature is being activated or deactivated. + * Allow order creations on the front end to get synced * - * @param string $feature Feature slug - * @param array $settings Feature settings - * @param array $data Feature activation data - * - * @return void + * @param bool $override Original order perms check value + * @param int $post_id Post ID + * @return bool */ - public function after_update_feature( $feature, $settings, $data ) { - if ( 'woocommerce' !== $feature ) { - return; - } + public function bypass_order_permissions_check( $override, $post_id ) { + $searchable_post_types = $this->get_admin_searchable_post_types(); - if ( true === $data['active'] ) { - $this->epio_save_search_template(); - } else { - $this->epio_delete_search_template(); + if ( in_array( get_post_type( $post_id ), $searchable_post_types, true ) ) { + return true; } - } - - /** - * Save the search template to ElasticPress.io. - * - * @return void - */ - public function epio_save_search_template() { - $endpoint = $this->get_template_endpoint(); - $template = $this->get_search_template(); - Elasticsearch::factory()->remote_request( - $endpoint, - [ - 'blocking' => false, - 'body' => $template, - 'method' => 'PUT', - ] - ); - - /** - * Fires after the request is sent the search template API endpoint. - * - * @since 4.5.0 - * @hook ep_woocommerce_order_search_template_saved - * @param {string} $template The search template (JSON). - * @param {string} $index Index name. - */ - do_action( 'ep_woocommerce_order_search_template_saved', $template, $this->index ); + return $override; } /** - * Delete the search template from ElasticPress.io. + * Returns the WooCommerce-oriented post types in admin that EP will search * - * @return void + * @return array */ - public function epio_delete_search_template() { - $endpoint = $this->get_template_endpoint(); - - Elasticsearch::factory()->remote_request( - $endpoint, - [ - 'blocking' => false, - 'method' => 'DELETE', - ] - ); + public function get_admin_searchable_post_types() { + $searchable_post_types = array( 'shop_order' ); /** - * Fires after the request is sent the search template API endpoint. + * Filter admin searchable WooCommerce post types * - * @since 4.5.0 - * @hook ep_woocommerce_order_search_template_deleted - * @param {string} $index Index name. + * @hook ep_woocommerce_admin_searchable_post_types + * @since 4.4.0 + * @param {array} $post_types Post types + * @return {array} New post types */ - do_action( 'ep_woocommerce_order_search_template_deleted', $this->index ); + return apply_filters( 'ep_woocommerce_admin_searchable_post_types', $searchable_post_types ); } /** - * Get the saved search template from ElasticPress.io. + * Index WooCommerce orders meta fields * - * @return string|WP_Error Search template if found, WP_Error on error. - */ - public function epio_get_search_template() { - $endpoint = $this->get_template_endpoint(); - $request = Elasticsearch::factory()->remote_request( $endpoint ); - - if ( is_wp_error( $request ) ) { - return $request; - } - - $response = wp_remote_retrieve_body( $request ); - - return $response; - } - - /** - * Generate a search template. - * - * A search template is the JSON for an Elasticsearch query with a - * placeholder search term. The template is sent to ElasticPress.io where - * it's used to make Elasticsearch queries using search terms sent from - * the front end. - * - * @return string The search template as JSON. + * @param array $meta Existing post meta + * @return array */ - public function get_search_template() { - $order_statuses = wc_get_order_statuses(); - - add_filter( 'ep_bypass_exclusion_from_search', '__return_true', 10 ); - add_filter( 'ep_intercept_remote_request', '__return_true' ); - add_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10, 4 ); - add_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10, 2 ); - - $query = new \WP_Query( - array( - 'ep_integrate' => true, - 'ep_order_search_template' => true, - 'post_status' => array_keys( $order_statuses ), - 'post_type' => 'shop_order', - 's' => '{{ep_placeholder}}', + public function allow_meta_keys( $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', + ) ) ); - - remove_filter( 'ep_bypass_exclusion_from_search', '__return_true', 10 ); - remove_filter( 'ep_intercept_remote_request', '__return_true' ); - remove_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10 ); - remove_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10 ); - - return $this->search_template; } /** - * Return true if a given feature is supported by WooCommerce Orders. + * Add order items as a searchable string. * - * Applied as a filter on Utils\is_integrated_request() so that features - * are enabled for the query that is used to generate the search template, - * regardless of the request type. This avoids the need to send a request - * to the front end. + * 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 bool $is_integrated Whether queries for the request will be - * integrated. - * @param string $context Context for the original check. Usually the - * slug of the feature doing the check. - * @return bool True if the check is for a feature supported by WooCommerce - * Order search. - */ - public function is_integrated_request( $is_integrated, $context ) { - $supported_contexts = [ - 'search', - 'woocommerce', - ]; - - return in_array( $context, $supported_contexts, true ); - } - - /** - * Store intercepted request body and return request result. + * @param array $post_args Post arguments + * @param string|int $post_id Post id * - * @param object $response Response - * @param array $query Query - * @param array $args WP_Query argument array - * @param int $failures Count of failures in request loop - * @return object $response Response - */ - public function intercept_search_request( $response, $query = [], $args = [], $failures = 0 ) { - $this->search_template = $query['args']['body']; - - return wp_remote_request( $query['url'], $args ); - } - - /** - * Get schema for search args. - * - * @return array Search args schema. - */ - public function get_args_schema() { - $args = array( - 'customer' => array( - 'type' => 'number', - ), - 'm' => array( - 'type' => 'string', - ), - 'offset' => array( - 'type' => 'number', - 'default' => 0, - ), - 'per_page' => array( - 'type' => 'number', - 'default' => 6, - ), - 'search' => array( - 'type' => 'string', - 'default' => '', - ), - ); - - return $args; - } - - /** - * Get a temporary token. - * - * @return string|false Authorization header, or false on failure. - */ - public function get_token() { - $user_id = get_current_user_id(); - - $credentials = get_user_meta( $user_id, 'ep_token', true ); - - if ( $credentials ) { - return $credentials; - } - - return $this->refresh_token(); - } - - /** - * Refresh the temporary token. - * - * @return string|false Authorization header, or false on failure. - */ - public function refresh_token() { - $user_id = get_current_user_id(); - - $endpoint = $this->get_token_endpoint(); - $response = Elasticsearch::factory()->remote_request( $endpoint, [ 'method' => 'POST' ] ); - - if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { - return false; - } - - $response = wp_remote_retrieve_body( $response ); - $response = json_decode( $response ); - - $credentials = base64_encode( "$response->username:$response->clear_password" ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode - - update_user_meta( $user_id, 'ep_token', $credentials ); - - return $credentials; - } - - /** - * Checks if the token API can be used. - * - * @return boolean Whether the token API can be used. - */ - public function check_token_permission() { - /** - * Filters the capability required to use the token API. - * - * @since 4.5.0 - * @hook ep_token_capability - * @param {string} $capability Required capability. - */ - $capability = apply_filters( 'ep_token_capability', 'edit_others_shop_orders' ); - - return current_user_can( $capability ); - } - - /** - * Index shop orders. - * - * @param array $post_types Indexable post types. - * @return array Indexable post types. - */ - public function post_types( $post_types ) { - $post_types['shop_order'] = 'shop_order'; - - return $post_types; - } - - /** - * Index order statuses. - * - * @param array $post_statuses Indexable post statuses. - * @return array Indexable post statuses. - */ - public function post_statuses( $post_statuses ) { - $order_statuses = wc_get_order_statuses(); - - return array_unique( array_merge( $post_statuses, array_keys( $order_statuses ) ) ); - } - - /** - * Add term suggestions to be indexed - * - * @param array $post_args Array of ES args. * @return array */ - public function filter_term_suggest( $post_args ) { - if ( empty( $post_args['post_type'] ) || 'shop_order' !== $post_args['post_type'] ) { - return $post_args; - } + public function add_order_items_search( $post_args, $post_id ) { + $searchable_post_types = $this->get_admin_searchable_post_types(); - if ( empty( $post_args['meta'] ) ) { + // Make sure it is only WooCommerce orders we touch. + if ( ! in_array( $post_args['post_type'], $searchable_post_types, true ) ) { return $post_args; } - /** - * Add the order number as a meta (text) field, so we can freely search on it. - */ - $order_id = $post_args['ID']; - if ( function_exists( 'wc_get_order' ) ) { - $order = wc_get_order( $post_args['ID'] ); - if ( $order && is_a( $order, 'WC_Order' ) && method_exists( $order, 'get_order_number' ) ) { - $order_id = $order->get_order_number(); - } - } - - $post_args['meta']['order_number'] = [ - [ - 'raw' => $order_id, - 'value' => $order_id, - ], - ]; - - $suggest = []; - - $fields_to_ngram = [ - '_billing_email', - '_billing_last_name', - '_billing_first_name', - ]; + $post_indexable = Indexables::factory()->get( 'post' ); - foreach ( $fields_to_ngram as $field_to_ngram ) { - if ( ! empty( $post_args['meta'][ $field_to_ngram ] ) - && ! empty( $post_args['meta'][ $field_to_ngram ][0] ) - && ! empty( $post_args['meta'][ $field_to_ngram ][0]['value'] ) ) { - $suggest[] = $post_args['meta'][ $field_to_ngram ][0]['value']; + // Get order items. + $order = wc_get_order( $post_id ); + $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']; } } - if ( ! empty( $suggest ) ) { - $post_args['term_suggest'] = $suggest; - } + // 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; } /** - * Add mapping for suggest fields + * Prevent order fields from being removed. * - * @param array $mapping ES mapping. - * @return array - */ - public function mapping( $mapping ) { - $post_indexable = Indexables::factory()->get( 'post' ); - - $mapping = $post_indexable->add_ngram_analyzer( $mapping ); - $mapping = $post_indexable->add_term_suggest_field( $mapping ); - - return $mapping; - } - - /** - * Set the search_fields parameter in the search template. + * 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 array $search_fields Current search fields - * @param \WP_Query $query Query being executed - * @return array New search fields + * @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 set_search_fields( array $search_fields, \WP_Query $query ) : array { - $is_orders_search_template = (bool) $query->get( 'ep_order_search_template' ); + public function keep_order_fields( $skip, $post_args ) { + $searchable_post_types = $this->get_admin_searchable_post_types(); - if ( $is_orders_search_template ) { - $search_fields = [ - 'meta.order_number.value', - 'term_suggest', - 'meta' => [ - '_billing_email', - '_billing_last_name', - '_billing_first_name', - ], - ]; + if ( in_array( $post_args['post_type'], $searchable_post_types, true ) ) { + return true; } - return $search_fields; + return $skip; } /** - * Allow password protected to be indexed. + * Sets woocommerce meta search fields to an empty array if we are integrating the main query with ElasticSearch * - * If Protected Content is enabled, do nothing. Otherwise, allow pw protected posts to be indexed. - * The feature restricts it back in maybe_set_posts_where() + * 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 * - * @see maybe_set_posts_where() - * @param array $args WP_Query args - * @return array + * @param \WP_Query $query Current query */ - public function maybe_query_password_protected_posts( $args ) { - // Password protected posts are already being indexed, no need to do anything. - if ( isset( $args['has_password'] ) && is_null( $args['has_password'] ) ) { - return $args; + public function maybe_hook_woocommerce_search_fields( $query ) { + global $pagenow, $wp, $wc_list_table; + + if ( ! $this->woocommerce->should_integrate_with_query( $query ) ) { + return; } /** - * Set a flag in the query but allow it to index all password protected posts for now, - * so WP does not inject its own where clause. + * Determines actions to be applied, or removed, if doing a WooCommerce serarch + * + * @hook ep_woocommerce_hook_search_fields + * @since 4.4.0 */ - $args['ep_orders_has_password'] = true; - $args['has_password'] = null; + 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; + } - return $args; + remove_action( 'parse_query', [ $wc_list_table, 'search_custom_fields' ] ); } /** - * Restrict password protected posts back but allow orders. + * 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 * - * @see maybe_query_password_protected_posts - * @param string $where Current where clause - * @param WP_Query $query WP_Query - * @return string + * @param WP_Query $wp WP Query */ - public function maybe_set_posts_where( $where, $query ) { - global $wpdb; - - if ( ! $query->get( 'ep_orders_has_password' ) ) { - return $where; + public function search_order( $wp ) { + if ( ! $this->woocommerce->should_integrate_with_query( $wp ) ) { + return; } - $where .= " AND ( {$wpdb->posts}.post_password = '' OR {$wpdb->posts}.post_type = 'shop_order' )"; + global $pagenow; - return $where; + $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 } } diff --git a/includes/classes/Feature/WooCommerce/OrdersAutosuggest.php b/includes/classes/Feature/WooCommerce/OrdersAutosuggest.php new file mode 100644 index 000000000..c68766da4 --- /dev/null +++ b/includes/classes/Feature/WooCommerce/OrdersAutosuggest.php @@ -0,0 +1,605 @@ +index = Indexables::factory()->get( 'post' )->get_index_name(); + } + + /** + * Setup feature functionality. + * + * @return void + */ + public function setup() { + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] ); + add_filter( 'ep_after_update_feature', [ $this, 'after_update_feature' ], 10, 3 ); + add_filter( 'ep_after_sync_index', [ $this, 'epio_save_search_template' ] ); + add_filter( 'ep_saved_weighting_configuration', [ $this, 'epio_save_search_template' ] ); + add_filter( 'ep_indexable_post_status', [ $this, 'post_statuses' ] ); + add_filter( 'ep_indexable_post_types', [ $this, 'post_types' ] ); + add_action( 'rest_api_init', [ $this, 'rest_api_init' ] ); + add_filter( 'ep_post_sync_args', [ $this, 'filter_term_suggest' ], 10 ); + add_filter( 'ep_post_mapping', [ $this, 'mapping' ] ); + add_action( 'ep_woocommerce_shop_order_search_fields', [ $this, 'set_search_fields' ], 10, 2 ); + add_filter( 'ep_index_posts_args', [ $this, 'maybe_query_password_protected_posts' ] ); + add_filter( 'posts_where', [ $this, 'maybe_set_posts_where' ], 10, 2 ); + } + + /** + * Get the endpoint for WooCommerce Orders search. + * + * @return string WooCommerce orders search endpoint. + */ + public function get_search_endpoint() { + /** + * Filters the WooCommerce Orders search endpoint. + * + * @since 4.5.0 + * @hook ep_woocommerce_order_search_endpoint + * @param {string} $endpoint Endpoint path. + * @param {string} $index Elasticsearch index. + */ + return apply_filters( 'ep_woocommerce_order_search_endpoint', "api/v1/search/orders/{$this->index}", $this->index ); + } + + /** + * Get the endpoint for the WooCommerce Orders search template. + * + * @return string WooCommerce Orders search template endpoint. + */ + public function get_template_endpoint() { + /** + * Filters the WooCommerce Orders search template API endpoint. + * + * @since 4.5.0 + * @hook ep_woocommerce_order_search_template_endpoint + * @param {string} $endpoint Endpoint path. + * @param {string} $index Elasticsearch index. + * @returns {string} Search template API endpoint. + */ + return apply_filters( 'ep_woocommerce_order_search_template_endpoint', "api/v1/search/orders/{$this->index}/template", $this->index ); + } + + /** + * Get the endpoint for temporary tokens. + * + * @return string Temporary token endpoint. + */ + public function get_token_endpoint() { + /** + * Filters the temporary token API endpoint. + * + * @since 4.5.0 + * @hook ep_token_endpoint + * @param {string} $endpoint Endpoint path. + * @returns {string} Token API endpoint. + */ + return apply_filters( 'ep_token_endpoint', 'api/v1/token' ); + } + + /** + * Registers the API endpoint to get a token. + * + * @return void + */ + public function rest_api_init() { + register_rest_route( + 'elasticpress/v1', + 'token', + [ + [ + 'callback' => [ $this, 'get_token' ], + 'permission_callback' => [ $this, 'check_token_permission' ], + 'methods' => 'GET', + ], + [ + 'callback' => [ $this, 'refresh_token' ], + 'permission_callback' => [ $this, 'check_token_permission' ], + 'methods' => 'POST', + ], + ] + ); + } + + /** + * Enqueue admin assets. + * + * @param string $hook_suffix The current admin page. + */ + public function enqueue_admin_assets( $hook_suffix ) { + if ( 'edit.php' !== $hook_suffix ) { + return; + } + + if ( ! isset( $_GET['post_type'] ) || 'shop_order' !== $_GET['post_type'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; + } + + wp_enqueue_style( + 'elasticpress-woocommerce-order-search', + EP_URL . 'dist/css/woocommerce-order-search-styles.css', + Utils\get_asset_info( 'woocommerce-order-search-styles', 'dependencies' ), + Utils\get_asset_info( 'woocommerce-order-search-styles', 'version' ) + ); + + wp_enqueue_script( + 'elasticpress-woocommerce-order-search', + EP_URL . 'dist/js/woocommerce-order-search-script.js', + Utils\get_asset_info( 'woocommerce-order-search-script', 'dependencies' ), + Utils\get_asset_info( 'woocommerce-order-search-script', 'version' ), + true + ); + + wp_set_script_translations( 'elasticpress-woocommerce-order-search', 'elasticpress' ); + + $api_endpoint = $this->get_search_endpoint(); + $api_host = Utils\get_host(); + + wp_localize_script( + 'elasticpress-woocommerce-order-search', + 'epWooCommerceOrderSearch', + array( + 'adminUrl' => admin_url( 'post.php' ), + 'apiEndpoint' => $api_endpoint, + 'apiHost' => ( 0 !== strpos( $api_endpoint, 'http' ) ) ? trailingslashit( esc_url_raw( $api_host ) ) : '', + 'argsSchema' => $this->get_args_schema(), + 'credentialsApiUrl' => rest_url( 'elasticpress/v1/token' ), + 'credentialsNonce' => wp_create_nonce( 'wp_rest' ), + 'dateFormat' => wc_date_format(), + 'statusLabels' => wc_get_order_statuses(), + 'timeFormat' => wc_time_format(), + 'requestIdBase' => Utils\get_request_id_base(), + ) + ); + } + + /** + * Save or delete the search template on ElasticPress.io based on whether + * the WooCommerce feature is being activated or deactivated. + * + * @param string $feature Feature slug + * @param array $settings Feature settings + * @param array $data Feature activation data + * + * @return void + */ + public function after_update_feature( $feature, $settings, $data ) { + if ( 'woocommerce' !== $feature ) { + return; + } + + if ( true === $data['active'] ) { + $this->epio_save_search_template(); + } else { + $this->epio_delete_search_template(); + } + } + + /** + * Save the search template to ElasticPress.io. + * + * @return void + */ + public function epio_save_search_template() { + $endpoint = $this->get_template_endpoint(); + $template = $this->get_search_template(); + + Elasticsearch::factory()->remote_request( + $endpoint, + [ + 'blocking' => false, + 'body' => $template, + 'method' => 'PUT', + ] + ); + + /** + * Fires after the request is sent the search template API endpoint. + * + * @since 4.5.0 + * @hook ep_woocommerce_order_search_template_saved + * @param {string} $template The search template (JSON). + * @param {string} $index Index name. + */ + do_action( 'ep_woocommerce_order_search_template_saved', $template, $this->index ); + } + + /** + * Delete the search template from ElasticPress.io. + * + * @return void + */ + public function epio_delete_search_template() { + $endpoint = $this->get_template_endpoint(); + + Elasticsearch::factory()->remote_request( + $endpoint, + [ + 'blocking' => false, + 'method' => 'DELETE', + ] + ); + + /** + * Fires after the request is sent the search template API endpoint. + * + * @since 4.5.0 + * @hook ep_woocommerce_order_search_template_deleted + * @param {string} $index Index name. + */ + do_action( 'ep_woocommerce_order_search_template_deleted', $this->index ); + } + + /** + * Get the saved search template from ElasticPress.io. + * + * @return string|WP_Error Search template if found, WP_Error on error. + */ + public function epio_get_search_template() { + $endpoint = $this->get_template_endpoint(); + $request = Elasticsearch::factory()->remote_request( $endpoint ); + + if ( is_wp_error( $request ) ) { + return $request; + } + + $response = wp_remote_retrieve_body( $request ); + + return $response; + } + + /** + * Generate a search template. + * + * A search template is the JSON for an Elasticsearch query with a + * placeholder search term. The template is sent to ElasticPress.io where + * it's used to make Elasticsearch queries using search terms sent from + * the front end. + * + * @return string The search template as JSON. + */ + public function get_search_template() { + $order_statuses = wc_get_order_statuses(); + + add_filter( 'ep_bypass_exclusion_from_search', '__return_true', 10 ); + add_filter( 'ep_intercept_remote_request', '__return_true' ); + add_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10, 4 ); + add_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10, 2 ); + + $query = new \WP_Query( + array( + 'ep_integrate' => true, + 'ep_order_search_template' => true, + 'post_status' => array_keys( $order_statuses ), + 'post_type' => 'shop_order', + 's' => '{{ep_placeholder}}', + ) + ); + + remove_filter( 'ep_bypass_exclusion_from_search', '__return_true', 10 ); + remove_filter( 'ep_intercept_remote_request', '__return_true' ); + remove_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10 ); + remove_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10 ); + + return $this->search_template; + } + + /** + * Return true if a given feature is supported by WooCommerce Orders. + * + * Applied as a filter on Utils\is_integrated_request() so that features + * are enabled for the query that is used to generate the search template, + * regardless of the request type. This avoids the need to send a request + * to the front end. + * + * @param bool $is_integrated Whether queries for the request will be + * integrated. + * @param string $context Context for the original check. Usually the + * slug of the feature doing the check. + * @return bool True if the check is for a feature supported by WooCommerce + * Order search. + */ + public function is_integrated_request( $is_integrated, $context ) { + $supported_contexts = [ + 'search', + 'woocommerce', + ]; + + return in_array( $context, $supported_contexts, true ); + } + + /** + * Store intercepted request body and return request result. + * + * @param object $response Response + * @param array $query Query + * @param array $args WP_Query argument array + * @param int $failures Count of failures in request loop + * @return object $response Response + */ + public function intercept_search_request( $response, $query = [], $args = [], $failures = 0 ) { + $this->search_template = $query['args']['body']; + + return wp_remote_request( $query['url'], $args ); + } + + /** + * Get schema for search args. + * + * @return array Search args schema. + */ + public function get_args_schema() { + $args = array( + 'customer' => array( + 'type' => 'number', + ), + 'm' => array( + 'type' => 'string', + ), + 'offset' => array( + 'type' => 'number', + 'default' => 0, + ), + 'per_page' => array( + 'type' => 'number', + 'default' => 6, + ), + 'search' => array( + 'type' => 'string', + 'default' => '', + ), + ); + + return $args; + } + + /** + * Get a temporary token. + * + * @return string|false Authorization header, or false on failure. + */ + public function get_token() { + $user_id = get_current_user_id(); + + $credentials = get_user_meta( $user_id, 'ep_token', true ); + + if ( $credentials ) { + return $credentials; + } + + return $this->refresh_token(); + } + + /** + * Refresh the temporary token. + * + * @return string|false Authorization header, or false on failure. + */ + public function refresh_token() { + $user_id = get_current_user_id(); + + $endpoint = $this->get_token_endpoint(); + $response = Elasticsearch::factory()->remote_request( $endpoint, [ 'method' => 'POST' ] ); + + if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + return false; + } + + $response = wp_remote_retrieve_body( $response ); + $response = json_decode( $response ); + + $credentials = base64_encode( "$response->username:$response->clear_password" ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + + update_user_meta( $user_id, 'ep_token', $credentials ); + + return $credentials; + } + + /** + * Checks if the token API can be used. + * + * @return boolean Whether the token API can be used. + */ + public function check_token_permission() { + /** + * Filters the capability required to use the token API. + * + * @since 4.5.0 + * @hook ep_token_capability + * @param {string} $capability Required capability. + */ + $capability = apply_filters( 'ep_token_capability', 'edit_others_shop_orders' ); + + return current_user_can( $capability ); + } + + /** + * Index shop orders. + * + * @param array $post_types Indexable post types. + * @return array Indexable post types. + */ + public function post_types( $post_types ) { + $post_types['shop_order'] = 'shop_order'; + + return $post_types; + } + + /** + * Index order statuses. + * + * @param array $post_statuses Indexable post statuses. + * @return array Indexable post statuses. + */ + public function post_statuses( $post_statuses ) { + $order_statuses = wc_get_order_statuses(); + + return array_unique( array_merge( $post_statuses, array_keys( $order_statuses ) ) ); + } + + /** + * Add term suggestions to be indexed + * + * @param array $post_args Array of ES args. + * @return array + */ + public function filter_term_suggest( $post_args ) { + if ( empty( $post_args['post_type'] ) || 'shop_order' !== $post_args['post_type'] ) { + return $post_args; + } + + if ( empty( $post_args['meta'] ) ) { + return $post_args; + } + + /** + * Add the order number as a meta (text) field, so we can freely search on it. + */ + $order_id = $post_args['ID']; + if ( function_exists( 'wc_get_order' ) ) { + $order = wc_get_order( $post_args['ID'] ); + if ( $order && is_a( $order, 'WC_Order' ) && method_exists( $order, 'get_order_number' ) ) { + $order_id = $order->get_order_number(); + } + } + + $post_args['meta']['order_number'] = [ + [ + 'raw' => $order_id, + 'value' => $order_id, + ], + ]; + + $suggest = []; + + $fields_to_ngram = [ + '_billing_email', + '_billing_last_name', + '_billing_first_name', + ]; + + foreach ( $fields_to_ngram as $field_to_ngram ) { + if ( ! empty( $post_args['meta'][ $field_to_ngram ] ) + && ! empty( $post_args['meta'][ $field_to_ngram ][0] ) + && ! empty( $post_args['meta'][ $field_to_ngram ][0]['value'] ) ) { + $suggest[] = $post_args['meta'][ $field_to_ngram ][0]['value']; + } + } + + if ( ! empty( $suggest ) ) { + $post_args['term_suggest'] = $suggest; + } + + return $post_args; + } + + /** + * Add mapping for suggest fields + * + * @param array $mapping ES mapping. + * @return array + */ + public function mapping( $mapping ) { + $post_indexable = Indexables::factory()->get( 'post' ); + + $mapping = $post_indexable->add_ngram_analyzer( $mapping ); + $mapping = $post_indexable->add_term_suggest_field( $mapping ); + + return $mapping; + } + + /** + * Set the search_fields parameter in the search template. + * + * @param array $search_fields Current search fields + * @param \WP_Query $query Query being executed + * @return array New search fields + */ + public function set_search_fields( array $search_fields, \WP_Query $query ) : array { + $is_orders_search_template = (bool) $query->get( 'ep_order_search_template' ); + + if ( $is_orders_search_template ) { + $search_fields = [ + 'meta.order_number.value', + 'term_suggest', + 'meta' => [ + '_billing_email', + '_billing_last_name', + '_billing_first_name', + ], + ]; + } + + return $search_fields; + } + + /** + * Allow password protected to be indexed. + * + * If Protected Content is enabled, do nothing. Otherwise, allow pw protected posts to be indexed. + * The feature restricts it back in maybe_set_posts_where() + * + * @see maybe_set_posts_where() + * @param array $args WP_Query args + * @return array + */ + public function maybe_query_password_protected_posts( $args ) { + // Password protected posts are already being indexed, no need to do anything. + if ( isset( $args['has_password'] ) && is_null( $args['has_password'] ) ) { + return $args; + } + + /** + * Set a flag in the query but allow it to index all password protected posts for now, + * so WP does not inject its own where clause. + */ + $args['ep_orders_has_password'] = true; + $args['has_password'] = null; + + return $args; + } + + /** + * Restrict password protected posts back but allow orders. + * + * @see maybe_query_password_protected_posts + * @param string $where Current where clause + * @param WP_Query $query WP_Query + * @return string + */ + public function maybe_set_posts_where( $where, $query ) { + global $wpdb; + + if ( ! $query->get( 'ep_orders_has_password' ) ) { + return $where; + } + + $where .= " AND ( {$wpdb->posts}.post_password = '' OR {$wpdb->posts}.post_type = 'shop_order' )"; + + return $where; + } +} diff --git a/includes/classes/Feature/WooCommerce/Products.php b/includes/classes/Feature/WooCommerce/Products.php new file mode 100644 index 000000000..b94179dc8 --- /dev/null +++ b/includes/classes/Feature/WooCommerce/Products.php @@ -0,0 +1,585 @@ +woocommerce = $woocommerce; + } + + /** + * Setup product related hooks + */ + public function setup() { + add_action( 'ep_formatted_args', [ $this, 'price_filter' ], 10, 3 ); + add_filter( 'ep_prepare_meta_allowed_protected_keys', [ $this, 'allow_meta_keys' ] ); + add_filter( 'ep_sync_taxonomies', [ $this, 'sync_taxonomies' ] ); + add_filter( 'ep_term_suggest_post_type', [ $this, 'suggest_wc_add_post_type' ] ); + add_filter( 'ep_facet_include_taxonomies', [ $this, 'add_product_attributes' ] ); + add_filter( 'ep_weighting_fields_for_post_type', [ $this, 'add_product_attributes_to_weighting' ], 10, 2 ); + add_filter( 'ep_weighting_default_post_type_weights', [ $this, 'add_product_default_post_type_weights' ], 10, 2 ); + add_filter( 'ep_prepare_meta_data', [ $this, 'add_variations_skus_meta' ], 10, 2 ); + add_filter( 'request', [ $this, 'admin_product_list_request_query' ], 9 ); + + // Custom product ordering + add_action( 'ep_admin_notices', [ $this, 'maybe_display_notice_about_product_ordering' ] ); + add_action( 'woocommerce_after_product_ordering', [ $this, 'action_sync_on_woocommerce_sort_single' ], 10, 2 ); + + // Settings for Weight results by date + add_action( 'ep_weight_settings_after_search', [ $this, 'add_weight_settings_search' ] ); + add_filter( 'ep_is_decaying_enabled', [ $this, 'maybe_disable_decaying' ], 10, 3 ); + } + + /** + * Modifies main query to allow filtering by price with WooCommerce "Filter by price" widget. + * + * @param array $args ES args + * @param array $query_args WP_Query args + * @param WP_Query $query WP_Query object + * @return array + */ + public function price_filter( $args, $query_args, $query ) { + // Only can use widget on main query + if ( ! $query->is_main_query() ) { + return $args; + } + + // Only can use widget on shop, product taxonomy, or search + if ( ! is_shop() && ! is_product_taxonomy() && ! is_search() ) { + return $args; + } + + // phpcs:disable WordPress.Security.NonceVerification + if ( empty( $_GET['min_price'] ) && empty( $_GET['max_price'] ) ) { + return $args; + } + + $min_price = ! empty( $_GET['min_price'] ) ? sanitize_text_field( wp_unslash( $_GET['min_price'] ) ) : null; + $max_price = ! empty( $_GET['max_price'] ) ? sanitize_text_field( wp_unslash( $_GET['max_price'] ) ) : null; + // phpcs:enable WordPress.Security.NonceVerification + + if ( $query->is_search() ) { + /** + * This logic is iffy but the WC price filter widget is not intended for use with search anyway + */ + $old_query = $args['query']['bool']; + unset( $args['query']['bool']['should'] ); + + if ( ! empty( $min_price ) ) { + $args['query']['bool']['must'][0]['range']['meta._price.long']['gte'] = $min_price; + } + + if ( ! empty( $max_price ) ) { + $args['query']['bool']['must'][0]['range']['meta._price.long']['lte'] = $max_price; + } + + $args['query']['bool']['must'][0]['range']['meta._price.long']['boost'] = 2.0; + $args['query']['bool']['must'][1]['bool'] = $old_query; + } else { + unset( $args['query']['match_all'] ); + + $args['query']['range']['meta._price.long']['gte'] = ! empty( $min_price ) ? $min_price : 0; + + if ( ! empty( $min_price ) ) { + $args['query']['range']['meta._price.long']['gte'] = $min_price; + } + + if ( ! empty( $max_price ) ) { + $args['query']['range']['meta._price.long']['lte'] = $max_price; + } + + $args['query']['range']['meta._price.long']['boost'] = 2.0; + } + + return $args; + } + + /** + * Index WooCommerce products meta fields + * + * @param array $meta Existing post meta + * @return array + */ + public function allow_meta_keys( $meta ) { + return array_unique( + array_merge( + $meta, + array( + '_thumbnail_id', + '_product_attributes', + '_wpb_vc_js_status', + '_swatch_type', + 'total_sales', + '_downloadable', + '_virtual', + '_regular_price', + '_sale_price', + '_tax_status', + '_tax_class', + '_purchase_note', + '_featured', + '_weight', + '_length', + '_width', + '_height', + '_visibility', + '_sku', + '_sale_price_dates_from', + '_sale_price_dates_to', + '_price', + '_sold_individually', + '_manage_stock', + '_backorders', + '_stock', + '_upsell_ids', + '_crosssell_ids', + '_stock_status', + '_product_version', + '_product_tabs', + '_override_tab_layout', + '_suggested_price', + '_min_price', + '_variable_billing', + '_wc_average_rating', + '_product_image_gallery', + '_bj_lazy_load_skip_post', + '_min_variation_price', + '_max_variation_price', + '_min_price_variation_id', + '_max_price_variation_id', + '_min_variation_regular_price', + '_max_variation_regular_price', + '_min_regular_price_variation_id', + '_max_regular_price_variation_id', + '_min_variation_sale_price', + '_max_variation_sale_price', + '_min_sale_price_variation_id', + '_max_sale_price_variation_id', + '_default_attributes', + '_swatch_type_options', + '_variations_skus', + ) + ) + ); + } + + /** + * Index WooCommerce taxonomies + * + * @param array $taxonomies Index taxonomies array + * @return array + */ + public function sync_taxonomies( $taxonomies ) { + $woo_taxonomies = []; + + $product_type = get_taxonomy( 'product_type' ); + if ( false !== $product_type ) { + $woo_taxonomies[] = $product_type; + } + + $product_visibility = get_taxonomy( 'product_visibility' ); + if ( false !== $product_visibility ) { + $woo_taxonomies[] = $product_visibility; + } + + /** + * Note product_shipping_class, product_cat, and product_tag are already public. Make + * sure to index non-attribute taxonomies. + */ + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + if ( ! empty( $attribute_taxonomies ) ) { + foreach ( $attribute_taxonomies as $tax ) { + $name = wc_attribute_taxonomy_name( $tax->attribute_name ); + + if ( ! empty( $name ) ) { + if ( empty( $tax->attribute_ ) ) { + $woo_taxonomies[] = get_taxonomy( $name ); + } + } + } + } + + return array_merge( $taxonomies, $woo_taxonomies ); + } + + /** + * Add WC product post type to autosuggest + * + * @param array $post_types Array of post types (e.g. post, page) + * @return array + */ + public function suggest_wc_add_post_type( $post_types ) { + if ( ! in_array( 'product', $post_types, true ) ) { + $post_types[] = 'product'; + } + + return $post_types; + } + + /** + * Add WooCommerce Product Attributes to EP Facets. + * + * @param array $taxonomies Taxonomies array + * @return array + */ + public function add_product_attributes( $taxonomies = [] ) { + $attribute_names = wc_get_attribute_taxonomy_names(); + + foreach ( $attribute_names as $name ) { + if ( ! taxonomy_exists( $name ) ) { + continue; + } + $taxonomies[ $name ] = get_taxonomy( $name ); + } + + return $taxonomies; + } + + /** + * Add WooCommerce Fields to the Weighting Dashboard. + * + * @param array $fields Current weighting fields. + * @param string $post_type Current post type. + * @return array New fields. + */ + public function add_product_attributes_to_weighting( $fields, $post_type ) { + if ( 'product' !== $post_type ) { + return $fields; + } + + if ( ! empty( $fields['attributes']['children']['author_name'] ) ) { + unset( $fields['attributes']['children']['author_name'] ); + } + + $sku_key = 'meta._sku.value'; + + $fields['attributes']['children'][ $sku_key ] = array( + 'key' => $sku_key, + 'label' => __( 'SKU', 'elasticpress' ), + ); + + $variations_skus_key = 'meta._variations_skus.value'; + + $fields['attributes']['children'][ $variations_skus_key ] = array( + 'key' => $variations_skus_key, + 'label' => __( 'Variations SKUs', 'elasticpress' ), + ); + + return $fields; + } + + /** + * Add WooCommerce Fields to the default values of the Weighting Dashboard. + * + * @param array $defaults Default values for the post type. + * @param string $post_type Current post type. + * @return array + */ + public function add_product_default_post_type_weights( $defaults, $post_type ) { + if ( 'product' !== $post_type ) { + return $defaults; + } + + if ( ! empty( $defaults['author_name'] ) ) { + unset( $defaults['author_name'] ); + } + + $defaults['meta._sku.value'] = array( + 'enabled' => true, + 'weight' => 1, + ); + + $defaults['meta._variations_skus.value'] = array( + 'enabled' => true, + 'weight' => 1, + ); + + return $defaults; + } + + /** + * Add a new `_variations_skus` meta field to the product to be indexed in Elasticsearch. + * + * @param array $post_meta Post meta + * @param WP_Post $post Post object + * @return array + */ + public function add_variations_skus_meta( $post_meta, $post ) { + if ( 'product' !== $post->post_type ) { + return $post_meta; + } + + $product = wc_get_product( $post ); + if ( ! $product ) { + return $post_meta; + } + + $variations_ids = $product->get_children(); + + $post_meta['_variations_skus'] = array_reduce( + $variations_ids, + function ( $variations_skus, $current_id ) { + $variation = wc_get_product( $current_id ); + if ( ! $variation || ! $variation->exists() ) { + return $variations_skus; + } + $variation_sku = $variation->get_sku(); + if ( ! $variation_sku ) { + return $variations_skus; + } + $variations_skus[] = $variation_sku; + return $variations_skus; + }, + [] + ); + + return $post_meta; + } + + /** + * Integrate ElasticPress with the WooCommerce Admin Product List. + * + * WooCommerce uses its `WC_Admin_List_Table_Products` class to control that screen. This + * function adds all necessary hooks to bypass the default behavior and integrate with ElasticPress. + * By default, WC runs a SQL query to get the Product IDs that match the list criteria and passes + * that list of IDs to the main WP_Query. This integration changes that process to a single query, run + * by ElasticPress. + * + * @param array $query_vars Query vars. + * @return array + */ + public function admin_product_list_request_query( $query_vars ) { + global $typenow, $wc_list_table; + + // Return if not in the correct screen. + if ( ! is_a( $wc_list_table, 'WC_Admin_List_Table_Products' ) || 'product' !== $typenow ) { + return $query_vars; + } + + // Return if admin WP_Query integration is not turned on, i.e., Protect Content is not enabled. + if ( ! has_filter( 'ep_admin_wp_query_integration', '__return_true' ) ) { + return $query_vars; + } + + /** + * Filter to skip integration with WooCommerce Admin Product List. + * + * @hook ep_woocommerce_integrate_admin_products_list + * @since 4.2.0 + * @param {bool} $integrate True to integrate, false to preserve original behavior. Defaults to true. + * @param {array} $query_vars Query vars. + * @return {bool} New integrate value + */ + if ( ! apply_filters( 'ep_woocommerce_integrate_admin_products_list', true, $query_vars ) ) { + return $query_vars; + } + + add_action( 'pre_get_posts', [ $this, 'translate_args_admin_products_list' ], 12 ); + + // This short-circuits WooCommerce search for product IDs. + add_filter( 'woocommerce_product_pre_search_products', '__return_empty_array' ); + + return $query_vars; + } + + /** + * Apply the necessary changes to WP_Query in WooCommerce Admin Product List. + * + * @param WP_Query $query The WP Query being executed. + */ + public function translate_args_admin_products_list( $query ) { + // The `translate_args()` method sets it to `true` if we should integrate it. + if ( ! $query->get( 'ep_integrate', false ) ) { + return; + } + + // WooCommerce unsets the search term right after using it to fetch product IDs. Here we add it back. + $search_term = ! empty( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification + if ( ! empty( $search_term ) ) { + $query->set( 's', sanitize_text_field( $search_term ) ); // phpcs:ignore WordPress.Security.NonceVerification + + /** + * Filter the fields used in WooCommerce Admin Product Search. + * + * ``` + * add_filter( + * 'ep_woocommerce_admin_products_list_search_fields', + * function ( $wc_admin_search_fields ) { + * $wc_admin_search_fields['meta'][] = 'custom_field'; + * return $wc_admin_search_fields; + * } + * ); + * ``` + * + * @hook ep_woocommerce_admin_products_list_search_fields + * @since 4.2.0 + * @param {array} $wc_admin_search_fields Fields to be used in the WooCommerce Admin Product Search + * @return {array} New fields + */ + $search_fields = apply_filters( + 'ep_woocommerce_admin_products_list_search_fields', + [ + 'post_title', + 'post_content', + 'post_excerpt', + 'meta' => [ + '_sku', + '_variations_skus', + ], + ] + ); + + $query->set( 'search_fields', $search_fields ); + } + + // 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()`. + $product_type_query = $query->get( 'product_type', '' ); + $product_type_url = ! empty( $_GET['product_type'] ) ? sanitize_text_field( wp_unslash( $_GET['product_type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification + $allowed_prod_types = [ 'virtual', 'downloadable' ]; + if ( empty( $product_type_query ) && ! empty( $product_type_url ) && in_array( $product_type_url, $allowed_prod_types, true ) ) { + $meta_query = $query->get( 'meta_query', [] ); + $meta_query[] = [ + 'key' => "_{$product_type_url}", + 'value' => 'yes', + ]; + $query->set( 'meta_query', $meta_query ); + } + + // Sets the meta query for `stock_status` if needed. + $stock_status_query = $query->get( 'stock_status', '' ); + $stock_status_url = ! empty( $_GET['stock_status'] ) ? sanitize_text_field( wp_unslash( $_GET['stock_status'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification + $allowed_stock_status = [ 'instock', 'outofstock', 'onbackorder' ]; + if ( empty( $stock_status_query ) && ! empty( $stock_status_url ) && in_array( $stock_status_url, $allowed_stock_status, true ) ) { + $meta_query = $query->get( 'meta_query', [] ); + $meta_query[] = [ + 'key' => '_stock_status', + 'value' => $stock_status_url, + ]; + $query->set( 'meta_query', $meta_query ); + } + } + + /** + * Depending on the number of products display an admin notice in the custom sort screen for WooCommerce Products + * + * @param array $notices Current ElasticPress admin notices + * @return array + */ + public function maybe_display_notice_about_product_ordering( $notices ) { + global $pagenow, $wp_query; + + /** + * Make sure we're on edit.php in admin dashboard. + */ + if ( ! is_admin() || 'edit.php' !== $pagenow || empty( $wp_query->query['orderby'] ) || 'menu_order title' !== $wp_query->query['orderby'] ) { + return $notices; + } + + $documents_per_page_sync = IndexHelper::factory()->get_index_default_per_page(); + if ( $documents_per_page_sync >= $wp_query->found_posts ) { + return $notices; + } + + $notices['woocommerce_custom_sort'] = [ + 'html' => sprintf( + /* translators: Sync Page URL */ + __( 'Due to the number of products in the site, you will need to resync after applying a custom sort order.', 'elasticpress' ), + Utils\get_sync_url() + ), + 'type' => 'warning', + 'dismiss' => true, + ]; + + return $notices; + } + + /** + * Conditionally resync products after applying a custom order. + * + * @param int $sorting_id ID of post dragged and dropped + * @param array $menu_orders Post IDs and their new menu_order value + */ + public function action_sync_on_woocommerce_sort_single( $sorting_id, $menu_orders ) { + $documents_per_page_sync = IndexHelper::factory()->get_index_default_per_page(); + if ( $documents_per_page_sync < count( $menu_orders ) ) { + return; + } + + $sync_manager = Indexables::factory()->get( 'post' )->sync_manager; + foreach ( $menu_orders as $post_id => $order ) { + $sync_manager->add_to_queue( $post_id ); + } + } + + /** + * Add weight by date settings related to WooCommerce + * + * @param array $settings Current settings. + */ + public function add_weight_settings_search( $settings ) { + ?> +
+ + 1 ) { + return $is_decaying_enabled; + } + + if ( 'disabled_includes_products' === $settings['decaying_enabled'] && ! in_array( 'product', $post_types, true ) ) { + return $is_decaying_enabled; + } + + return false; + } +} diff --git a/includes/classes/Feature/WooCommerce/WooCommerce.php b/includes/classes/Feature/WooCommerce/WooCommerce.php index 5bbfe41df..6ac5a7c68 100644 --- a/includes/classes/Feature/WooCommerce/WooCommerce.php +++ b/includes/classes/Feature/WooCommerce/WooCommerce.php @@ -23,7 +23,23 @@ */ class WooCommerce extends Feature { /** - * If enabled, receive the Orders object instance + * If enabled, receive the OrdersAutosuggest object instance + * + * @since 4.7.0 + * @var null|OrdersAutosuggest + */ + public $orders_autosuggest = null; + + /** + * Receive the Products object instance + * + * @since 4.7.0 + * @var null|Products + */ + public $products = null; + + /** + * Receive the Orders object instance * * @since 4.5.0 * @var null|Orders @@ -54,172 +70,39 @@ public function __construct() { 'orders' => '0', ]; - $this->orders = new Orders(); + $this->orders = new Orders( $this ); + $this->products = new Products( $this ); + $this->orders_autosuggest = new OrdersAutosuggest(); parent::__construct(); } /** - * Index Woocommerce meta - * - * @param array $meta Existing post meta. - * @param array $post Post arguments array. - * @since 2.1 - * @return array - */ - public function whitelist_meta_keys( $meta, $post ) { - return array_unique( - array_merge( - $meta, - array( - '_thumbnail_id', - '_product_attributes', - '_wpb_vc_js_status', - '_swatch_type', - 'total_sales', - '_downloadable', - '_virtual', - '_regular_price', - '_sale_price', - '_tax_status', - '_tax_class', - '_purchase_note', - '_featured', - '_weight', - '_length', - '_width', - '_height', - '_visibility', - '_sku', - '_sale_price_dates_from', - '_sale_price_dates_to', - '_price', - '_sold_individually', - '_manage_stock', - '_backorders', - '_stock', - '_upsell_ids', - '_crosssell_ids', - '_stock_status', - '_product_version', - '_product_tabs', - '_override_tab_layout', - '_suggested_price', - '_min_price', - '_customer_user', - '_variable_billing', - '_wc_average_rating', - '_product_image_gallery', - '_bj_lazy_load_skip_post', - '_min_variation_price', - '_max_variation_price', - '_min_price_variation_id', - '_max_price_variation_id', - '_min_variation_regular_price', - '_max_variation_regular_price', - '_min_regular_price_variation_id', - '_max_regular_price_variation_id', - '_min_variation_sale_price', - '_max_variation_sale_price', - '_min_sale_price_variation_id', - '_max_sale_price_variation_id', - '_default_attributes', - '_swatch_type_options', - '_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', - ) - ) - ); - } - - /** - * Make sure all loop shop post ins are IDS. We have to pass post objects here since we override - * the fields=>id query for the layered filter nav query - * - * @param array $posts Post object array. - * @since 2.1 - * @return array - */ - public function convert_post_object_to_id( $posts ) { - _doing_it_wrong( __METHOD__, 'This filter was removed from WooCommerce and will be removed from ElasticPress in a future release.', '4.5.0' ); - return $posts; - } - - /** - * Index Woocommerce taxonomies + * Setup all feature filters * - * @param array $taxonomies Index taxonomies array. - * @param array $post Post properties array. - * @since 2.1 - * @return array + * @since 2.1 */ - public function whitelist_taxonomies( $taxonomies, $post ) { - $woo_taxonomies = []; - - $product_type = get_taxonomy( 'product_type' ); - if ( false !== $product_type ) { - $woo_taxonomies[] = $product_type; - } - - $product_visibility = get_taxonomy( 'product_visibility' ); - if ( false !== $product_visibility ) { - $woo_taxonomies[] = $product_visibility; + public function setup() { + if ( ! function_exists( 'WC' ) ) { + return; } - /** - * Note product_shipping_class, product_cat, and product_tag are already public. Make - * sure to index non-attribute taxonomies. - */ - $attribute_taxonomies = wc_get_attribute_taxonomies(); - - if ( ! empty( $attribute_taxonomies ) ) { - foreach ( $attribute_taxonomies as $tax ) { - $name = wc_attribute_taxonomy_name( $tax->attribute_name ); + $this->products->setup(); + $this->orders->setup(); - if ( ! empty( $name ) ) { - if ( empty( $tax->attribute_ ) ) { - $woo_taxonomies[] = get_taxonomy( $name ); - } - } - } - } + add_filter( 'ep_integrate_search_queries', [ $this, 'disallow_coupons' ], 10, 2 ); - return array_merge( $taxonomies, $woo_taxonomies ); - } + // These hooks are deprecated and will be removed in an upcoming major version of ElasticPress + add_filter( 'woocommerce_layered_nav_query_post_ids', [ $this, 'convert_post_object_to_id' ], 10, 4 ); + add_filter( 'woocommerce_unfiltered_product_ids', [ $this, 'convert_post_object_to_id' ], 10, 4 ); + add_action( 'ep_wp_query_search_cached_posts', [ $this, 'disallow_duplicated_query' ], 10, 2 ); - /** - * Disallow duplicated ES queries on Orders page. - * - * @since 2.4 - * - * @param array $value Original filter values. - * @param WP_Query $query WP_Query - * - * @return array - */ - public function disallow_duplicated_query( $value, $query ) { - _doing_it_wrong( __METHOD__, 'This filter was removed from WooCommerce and will be removed from ElasticPress in a future release.', '4.5.0' ); + add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 ); - return $value; + // Orders Autosuggest feature. + if ( $this->is_orders_autosuggest_enabled() ) { + $this->orders_autosuggest->setup(); + } } /** @@ -591,36 +474,15 @@ public function get_orderby_meta_mapping( $meta_key ) { return 'date'; } - - /** - * Returns the WooCommerce-oriented post types in admin that EP will search - * - * @since 4.4.0 - * @return mixed|void - */ - 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 ); - } - /** * Make search coupons don't go through ES * * @param bool $enabled Coupons enabled or not * @param WP_Query $query WP Query - * @since 2.1 + * @since 4.7.0 * @return bool */ - public function blacklist_coupons( $enabled, $query ) { + public function disallow_coupons( $enabled, $query ) { if ( is_admin() ) { return $enabled; } @@ -632,271 +494,6 @@ public function blacklist_coupons( $enabled, $query ) { return $enabled; } - /** - * Allow order creations on the front end to get synced - * - * @since 2.1 - * @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; - } - - /** - * 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, $wp_filter; - - if ( ! $this->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 - * @since 2.3 - */ - public function search_order( $wp ) { - if ( ! $this->should_integrate_with_query( $wp ) ) { - return; - } - - global $pagenow; - - $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 - } - - /** - * 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". - * - * @since 2.4 - * - * @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 ) { - $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. - $order = wc_get_order( $post_id ); - $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; - } - - /** - * Add WooCommerce Product Attributes to EP Facets. - * - * @param array $taxonomies Taxonomies array - * @return array - */ - public function add_product_attributes( $taxonomies = [] ) { - $attribute_names = wc_get_attribute_taxonomy_names(); - - foreach ( $attribute_names as $name ) { - if ( ! taxonomy_exists( $name ) ) { - continue; - } - $taxonomies[ $name ] = get_taxonomy( $name ); - } - - return $taxonomies; - } - - /** - * Add WooCommerce Fields to the Weighting Dashboard. - * - * @since 3.x - * - * @param array $fields Current weighting fields. - * @param string $post_type Current post type. - * @return array New fields. - */ - public function add_product_attributes_to_weighting( $fields, $post_type ) { - if ( 'product' === $post_type ) { - if ( ! empty( $fields['attributes']['children']['author_name'] ) ) { - unset( $fields['attributes']['children']['author_name'] ); - } - - $sku_key = 'meta._sku.value'; - - $fields['attributes']['children'][ $sku_key ] = array( - 'key' => $sku_key, - 'label' => __( 'SKU', 'elasticpress' ), - ); - - $variations_skus_key = 'meta._variations_skus.value'; - - $fields['attributes']['children'][ $variations_skus_key ] = array( - 'key' => $variations_skus_key, - 'label' => __( 'Variations SKUs', 'elasticpress' ), - ); - } - return $fields; - } - - /** - * Add WooCommerce Fields to the default values of the Weighting Dashboard. - * - * @since 3.x - * - * @param array $defaults Default values for the post type. - * @param string $post_type Current post type. - * @return array - */ - public function add_product_default_post_type_weights( $defaults, $post_type ) { - if ( 'product' === $post_type ) { - if ( ! empty( $defaults['author_name'] ) ) { - unset( $defaults['author_name'] ); - } - - $defaults['meta._sku.value'] = array( - 'enabled' => true, - 'weight' => 1, - ); - - $defaults['meta._variations_skus.value'] = array( - 'enabled' => true, - 'weight' => 1, - ); - } - return $defaults; - } - - /** - * Add WC post type to autosuggest - * - * @param array $post_types Array of post types (e.g. post, page). - * @since 2.6 - * @return array - */ - public function suggest_wc_add_post_type( $post_types ) { - if ( ! in_array( 'product', $post_types, true ) ) { - $post_types[] = 'product'; - } - - return $post_types; - } - - /** - * Setup all feature filters - * - * @since 2.1 - */ - public function setup() { - if ( ! function_exists( 'WC' ) ) { - return; - } - - add_action( 'ep_formatted_args', [ $this, 'price_filter' ], 10, 3 ); - add_filter( 'ep_sync_insert_permissions_bypass', [ $this, 'bypass_order_permissions_check' ], 10, 2 ); - add_filter( 'ep_integrate_search_queries', [ $this, 'blacklist_coupons' ], 10, 2 ); - add_filter( 'ep_prepare_meta_allowed_protected_keys', [ $this, 'whitelist_meta_keys' ], 10, 2 ); - add_filter( 'woocommerce_layered_nav_query_post_ids', [ $this, 'convert_post_object_to_id' ], 10, 4 ); - add_filter( 'woocommerce_unfiltered_product_ids', [ $this, 'convert_post_object_to_id' ], 10, 4 ); - add_filter( 'ep_sync_taxonomies', [ $this, 'whitelist_taxonomies' ], 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( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 ); - add_action( 'ep_wp_query_search_cached_posts', [ $this, 'disallow_duplicated_query' ], 10, 2 ); - add_action( 'parse_query', [ $this, 'maybe_hook_woocommerce_search_fields' ], 1 ); - add_action( 'parse_query', [ $this, 'search_order' ], 11 ); - add_filter( 'ep_term_suggest_post_type', [ $this, 'suggest_wc_add_post_type' ] ); - add_filter( 'ep_facet_include_taxonomies', [ $this, 'add_product_attributes' ] ); - add_filter( 'ep_weighting_fields_for_post_type', [ $this, 'add_product_attributes_to_weighting' ], 10, 2 ); - add_filter( 'ep_weighting_default_post_type_weights', [ $this, 'add_product_default_post_type_weights' ], 10, 2 ); - add_filter( 'ep_prepare_meta_data', [ $this, 'add_variations_skus_meta' ], 10, 2 ); - add_filter( 'request', [ $this, 'admin_product_list_request_query' ], 9 ); - - // Custom product ordering - add_action( 'ep_admin_notices', [ $this, 'maybe_display_notice_about_product_ordering' ] ); - add_action( 'woocommerce_after_product_ordering', [ $this, 'action_sync_on_woocommerce_sort_single' ], 10, 2 ); - - // Orders Autosuggest feature. - if ( $this->is_orders_autosuggest_enabled() ) { - $this->orders->setup(); - } - - // Add WooCommerce Settings for Weight results by date - add_action( 'ep_weight_settings_after_search', [ $this, 'add_weight_settings_search' ] ); - // Modify decaying based on WooCommerce Settings - add_filter( 'ep_is_decaying_enabled', [ $this, 'maybe_disable_decaying' ], 10, 3 ); - } - /** * Output feature box long * @@ -981,265 +578,13 @@ public function requirements_status() { } /** - * Modifies main query to allow filtering by price with WooCommerce "Filter by price" widget. - * - * @param array $args ES args - * @param array $query_args WP_Query args - * @param WP_Query $query WP_Query object - * @since 3.2 - * @return array - */ - public function price_filter( $args, $query_args, $query ) { - // Only can use widget on main query - if ( ! $query->is_main_query() ) { - return $args; - } - - // Only can use widget on shop, product taxonomy, or search - if ( ! is_shop() && ! is_product_taxonomy() && ! is_search() ) { - return $args; - } - - // phpcs:disable WordPress.Security.NonceVerification - if ( empty( $_GET['min_price'] ) && empty( $_GET['max_price'] ) ) { - return $args; - } - - $min_price = ! empty( $_GET['min_price'] ) ? sanitize_text_field( wp_unslash( $_GET['min_price'] ) ) : null; - $max_price = ! empty( $_GET['max_price'] ) ? sanitize_text_field( wp_unslash( $_GET['max_price'] ) ) : null; - // phpcs:enable WordPress.Security.NonceVerification - - if ( $query->is_search() ) { - /** - * This logic is iffy but the WC price filter widget is not intended for use with search anyway - */ - $old_query = $args['query']['bool']; - unset( $args['query']['bool']['should'] ); - - if ( ! empty( $min_price ) ) { - $args['query']['bool']['must'][0]['range']['meta._price.long']['gte'] = $min_price; - } - - if ( ! empty( $max_price ) ) { - $args['query']['bool']['must'][0]['range']['meta._price.long']['lte'] = $max_price; - } - - $args['query']['bool']['must'][0]['range']['meta._price.long']['boost'] = 2.0; - $args['query']['bool']['must'][1]['bool'] = $old_query; - } else { - unset( $args['query']['match_all'] ); - - $args['query']['range']['meta._price.long']['gte'] = ! empty( $min_price ) ? $min_price : 0; - - if ( ! empty( $min_price ) ) { - $args['query']['range']['meta._price.long']['gte'] = $min_price; - } - - if ( ! empty( $max_price ) ) { - $args['query']['range']['meta._price.long']['lte'] = $max_price; - } - - $args['query']['range']['meta._price.long']['boost'] = 2.0; - } - - return $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 - * - * @since 4.2.0 - * @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; - } - - /** - * Add a new `_variations_skus` meta field to the product to be indexed in Elasticsearch. - * - * @since 4.2.0 - * @param array $post_meta Post meta - * @param WP_Post $post Post object - * @return array - */ - public function add_variations_skus_meta( $post_meta, $post ) { - if ( 'product' !== $post->post_type ) { - return $post_meta; - } - - $product = wc_get_product( $post ); - if ( ! $product ) { - return $post_meta; - } - - $variations_ids = $product->get_children(); - - $post_meta['_variations_skus'] = array_reduce( - $variations_ids, - function ( $variations_skus, $current_id ) { - $variation = wc_get_product( $current_id ); - if ( ! $variation || ! $variation->exists() ) { - return $variations_skus; - } - $variation_sku = $variation->get_sku(); - if ( ! $variation_sku ) { - return $variations_skus; - } - $variations_skus[] = $variation_sku; - return $variations_skus; - }, - [] - ); - - return $post_meta; - } - - /** - * Integrate ElasticPress with the WooCommerce Admin Product List. - * - * WooCommerce uses its `WC_Admin_List_Table_Products` class to control that screen. This - * function adds all necessary hooks to bypass the default behavior and integrate with ElasticPress. - * By default, WC runs a SQL query to get the Product IDs that match the list criteria and passes - * that list of IDs to the main WP_Query. This integration changes that process to a single query, run - * by ElasticPress. - * - * @since 4.2.0 - * @param array $query_vars Query vars. - * @return array - */ - public function admin_product_list_request_query( $query_vars ) { - global $typenow, $wc_list_table; - - // Return if not in the correct screen. - if ( ! is_a( $wc_list_table, 'WC_Admin_List_Table_Products' ) || 'product' !== $typenow ) { - return $query_vars; - } - - // Return if admin WP_Query integration is not turned on, i.e., Protect Content is not enabled. - if ( ! has_filter( 'ep_admin_wp_query_integration', '__return_true' ) ) { - return $query_vars; - } - - /** - * Filter to skip integration with WooCommerce Admin Product List. - * - * @hook ep_woocommerce_integrate_admin_products_list - * @since 4.2.0 - * @param {bool} $integrate True to integrate, false to preserve original behavior. Defaults to true. - * @param {array} $query_vars Query vars. - * @return {bool} New integrate value - */ - if ( ! apply_filters( 'ep_woocommerce_integrate_admin_products_list', true, $query_vars ) ) { - return $query_vars; - } - - add_action( 'pre_get_posts', [ $this, 'translate_args_admin_products_list' ], 12 ); - - // This short-circuits WooCommerce search for product IDs. - add_filter( 'woocommerce_product_pre_search_products', '__return_empty_array' ); - - return $query_vars; - } - - /** - * Apply the necessary changes to WP_Query in WooCommerce Admin Product List. - * - * @param WP_Query $query The WP Query being executed. - */ - public function translate_args_admin_products_list( $query ) { - // The `translate_args()` method sets it to `true` if we should integrate it. - if ( ! $query->get( 'ep_integrate', false ) ) { - return; - } - - // WooCommerce unsets the search term right after using it to fetch product IDs. Here we add it back. - $search_term = ! empty( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification - if ( ! empty( $search_term ) ) { - $query->set( 's', sanitize_text_field( $search_term ) ); // phpcs:ignore WordPress.Security.NonceVerification - - /** - * Filter the fields used in WooCommerce Admin Product Search. - * - * ``` - * add_filter( - * 'ep_woocommerce_admin_products_list_search_fields', - * function ( $wc_admin_search_fields ) { - * $wc_admin_search_fields['meta'][] = 'custom_field'; - * return $wc_admin_search_fields; - * } - * ); - * ``` - * - * @hook ep_woocommerce_admin_products_list_search_fields - * @since 4.2.0 - * @param {array} $wc_admin_search_fields Fields to be used in the WooCommerce Admin Product Search - * @return {array} New fields - */ - $search_fields = apply_filters( - 'ep_woocommerce_admin_products_list_search_fields', - [ - 'post_title', - 'post_content', - 'post_excerpt', - 'meta' => [ - '_sku', - '_variations_skus', - ], - ] - ); - - $query->set( 'search_fields', $search_fields ); - } - - // 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()`. - $product_type_query = $query->get( 'product_type', '' ); - $product_type_url = ! empty( $_GET['product_type'] ) ? sanitize_text_field( wp_unslash( $_GET['product_type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification - $allowed_prod_types = [ 'virtual', 'downloadable' ]; - if ( empty( $product_type_query ) && ! empty( $product_type_url ) && in_array( $product_type_url, $allowed_prod_types, true ) ) { - $meta_query = $query->get( 'meta_query', [] ); - $meta_query[] = [ - 'key' => "_{$product_type_url}", - 'value' => 'yes', - ]; - $query->set( 'meta_query', $meta_query ); - } - - // Sets the meta query for `stock_status` if needed. - $stock_status_query = $query->get( 'stock_status', '' ); - $stock_status_url = ! empty( $_GET['stock_status'] ) ? sanitize_text_field( wp_unslash( $_GET['stock_status'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification - $allowed_stock_status = [ 'instock', 'outofstock', 'onbackorder' ]; - if ( empty( $stock_status_query ) && ! empty( $stock_status_url ) && in_array( $stock_status_url, $allowed_stock_status, true ) ) { - $meta_query = $query->get( 'meta_query', [] ); - $meta_query[] = [ - 'key' => '_stock_status', - 'value' => $stock_status_url, - ]; - $query->set( 'meta_query', $meta_query ); - } - } - - /** - * Determines whether or not ES should be integrating with the provided query + * Determines whether or not ES should be integrating with the provided query * * @param \WP_Query $query Query we might integrate with * * @return bool */ - protected function should_integrate_with_query( $query ) { + public function should_integrate_with_query( $query ) { // Lets make sure this doesn't interfere with the CLI if ( defined( 'WP_CLI' ) && WP_CLI ) { return false; @@ -1295,61 +640,6 @@ protected function should_integrate_with_query( $query ) { return true; } - /** - * Depending on the number of products display an admin notice in the custom sort screen for WooCommerce Products - * - * @since 4.4.0 - * @param array $notices Current ElasticPress admin notices - * @return array - */ - public function maybe_display_notice_about_product_ordering( $notices ) { - global $pagenow, $wp_query; - - /** - * Make sure we're on edit.php in admin dashboard. - */ - if ( ! is_admin() || 'edit.php' !== $pagenow || empty( $wp_query->query['orderby'] ) || 'menu_order title' !== $wp_query->query['orderby'] ) { - return $notices; - } - - $documents_per_page_sync = IndexHelper::factory()->get_index_default_per_page(); - if ( $documents_per_page_sync >= $wp_query->found_posts ) { - return $notices; - } - - $notices['woocommerce_custom_sort'] = [ - 'html' => sprintf( - /* translators: Sync Page URL */ - __( 'Due to the number of products in the site, you will need to resync after applying a custom sort order.', 'elasticpress' ), - Utils\get_sync_url() - ), - 'type' => 'warning', - 'dismiss' => true, - ]; - - return $notices; - } - - /** - * Conditionally resync products after applying a custom order. - * - * @since 4.4.0 - * @param int $sorting_id ID of post dragged and dropped - * @param array $menu_orders Post IDs and their new menu_order value - */ - public function action_sync_on_woocommerce_sort_single( $sorting_id, $menu_orders ) { - - $documents_per_page_sync = IndexHelper::factory()->get_index_default_per_page(); - if ( $documents_per_page_sync < count( $menu_orders ) ) { - return; - } - - $sync_manager = Indexables::factory()->get( 'post' )->sync_manager; - foreach ( $menu_orders as $post_id => $order ) { - $sync_manager->add_to_queue( $post_id ); - } - } - /** * Whether orders autosuggest is available or not * @@ -1378,21 +668,314 @@ public function is_orders_autosuggest_enabled() : bool { return $this->is_orders_autosuggest_available() && '1' === $this->get_setting( 'orders' ); } + + /** + * DEPRECATED. Index Woocommerce meta + * + * @param array $meta Existing post meta. + * @param array $post Post arguments array. + * @since 2.1 + * @return array + */ + public function whitelist_meta_keys( $meta, $post ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->allow_meta_keys() AND/OR \ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders->allow_meta_keys()" ); + return array_unique( + array_merge( + $this->products->allow_meta_keys( $meta ), + $this->orders->allow_meta_keys( $meta ) + ) + ); + } + + /** + * DEPRECATED. Make sure all loop shop post ins are IDS. We have to pass post objects here since we override + * the fields=>id query for the layered filter nav query + * + * @param array $posts Post object array. + * @since 2.1 + * @return array + */ + public function convert_post_object_to_id( $posts ) { + _doing_it_wrong( __METHOD__, 'This filter was removed from WooCommerce and will be removed from ElasticPress in a future release.', '4.5.0' ); + return $posts; + } + /** - * Add weight by date settings related to WooCommerce + * DEPRECATED. Index Woocommerce taxonomies + * + * @param array $taxonomies Index taxonomies array. + * @param array $post Post properties array. + * @since 2.1 + * @return array + */ + public function whitelist_taxonomies( $taxonomies, $post ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->sync_taxonomies()" ); + return $this->products->sync_taxonomies( $taxonomies ); + } + + /** + * DEPRECATED. Disallow duplicated ES queries on Orders page. + * + * @since 2.4 + * + * @param array $value Original filter values. + * @param WP_Query $query WP_Query + * + * @return array + */ + public function disallow_duplicated_query( $value, $query ) { + _doing_it_wrong( __METHOD__, 'This filter was removed from WooCommerce and will be removed from ElasticPress in a future release.', '4.5.0' ); + + return $value; + } + + /** + * DEPRECATED. Returns the WooCommerce-oriented post types in admin that EP will search + * + * @since 4.4.0 + * @return mixed|void + */ + public function get_admin_searchable_post_types() { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders->get_admin_searchable_post_types()" ); + return $this->orders->get_admin_searchable_post_types(); + } + + /** + * DEPRECATED. Make search coupons don't go through ES + * + * @param bool $enabled Coupons enabled or not + * @param WP_Query $query WP Query + * @since 2.1 + * @return bool + */ + public function blacklist_coupons( $enabled, $query ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->disallow_coupons()" ); + return $this->disallow_coupons( $enabled, $query ); + } + + /** + * DEPRECATED. Allow order creations on the front end to get synced + * + * @since 2.1 + * @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 ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders->price_filter()" ); + return $this->orders->bypass_order_permissions_check( $override, $post_id ); + } + + /** + * DEPRECATED. 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 ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders->maybe_hook_woocommerce_search_fields()" ); + return $this->orders->maybe_hook_woocommerce_search_fields( $query ); + } + + /** + * DEPRECATED. 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 + * @since 2.3 + */ + public function search_order( $wp ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders->search_order()" ); + return $this->orders->search_order( $wp ); + } + + /** + * DEPRECATED. 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". + * + * @since 2.4 + * + * @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 ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders->add_order_items_search()" ); + return $this->orders->add_order_items_search( $post_args, $post_id ); + } + + /** + * DEPRECATED. Add WooCommerce Product Attributes to EP Facets. + * + * @param array $taxonomies Taxonomies array + * @return array + */ + public function add_product_attributes( $taxonomies = [] ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->add_product_attributes()" ); + return $this->products->add_product_attributes( $taxonomies ); + } + + /** + * DEPRECATED. Add WooCommerce Fields to the Weighting Dashboard. + * + * @since 3.x + * + * @param array $fields Current weighting fields. + * @param string $post_type Current post type. + * @return array New fields. + */ + public function add_product_attributes_to_weighting( $fields, $post_type ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->add_product_attributes_to_weighting()" ); + return $this->products->add_product_attributes_to_weighting( $fields, $post_type ); + } + + /** + * DEPRECATED. Add WooCommerce Fields to the default values of the Weighting Dashboard. + * + * @since 3.x + * + * @param array $defaults Default values for the post type. + * @param string $post_type Current post type. + * @return array + */ + public function add_product_default_post_type_weights( $defaults, $post_type ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->add_product_default_post_type_weights()" ); + return $this->products->add_product_default_post_type_weights( $defaults, $post_type ); + } + + /** + * DEPRECATED. Add WC post type to autosuggest + * + * @param array $post_types Array of post types (e.g. post, page). + * @since 2.6 + * @return array + */ + public function suggest_wc_add_post_type( $post_types ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->suggest_wc_add_post_type()" ); + return $this->products->suggest_wc_add_post_type( $post_types ); + } + + /** + * DEPRECATED. Modifies main query to allow filtering by price with WooCommerce "Filter by price" widget. + * + * @param array $args ES args + * @param array $query_args WP_Query args + * @param WP_Query $query WP_Query object + * @since 3.2 + * @return array + */ + public function price_filter( $args, $query_args, $query ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->price_filter()" ); + return $this->products->price_filter( $args, $query_args, $query ); + } + + /** + * DEPRECATED. 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 + * + * @since 4.2.0 + * @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 ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders->keep_order_fields()" ); + return $this->orders->keep_order_fields( $skip, $post_args ); + } + + /** + * DEPRECATED. Add a new `_variations_skus` meta field to the product to be indexed in Elasticsearch. + * + * @since 4.2.0 + * @param array $post_meta Post meta + * @param WP_Post $post Post object + * @return array + */ + public function add_variations_skus_meta( $post_meta, $post ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->add_variations_skus_meta()" ); + return $this->products->add_variations_skus_meta( $post_meta, $post ); + } + + /** + * DEPRECATED. Integrate ElasticPress with the WooCommerce Admin Product List. + * + * WooCommerce uses its `WC_Admin_List_Table_Products` class to control that screen. This + * function adds all necessary hooks to bypass the default behavior and integrate with ElasticPress. + * By default, WC runs a SQL query to get the Product IDs that match the list criteria and passes + * that list of IDs to the main WP_Query. This integration changes that process to a single query, run + * by ElasticPress. + * + * @since 4.2.0 + * @param array $query_vars Query vars. + * @return array + */ + public function admin_product_list_request_query( $query_vars ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->admin_product_list_request_query()" ); + return $this->products->admin_product_list_request_query( $query_vars ); + } + + /** + * DEPRECATED. Apply the necessary changes to WP_Query in WooCommerce Admin Product List. + * + * @param WP_Query $query The WP Query being executed. + */ + public function translate_args_admin_products_list( $query ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->price_filter()" ); + $this->products->translate_args_admin_products_list( $query ); + } + + /** + * DEPRECATED. Depending on the number of products display an admin notice in the custom sort screen for WooCommerce Products + * + * @since 4.4.0 + * @param array $notices Current ElasticPress admin notices + * @return array + */ + public function maybe_display_notice_about_product_ordering( $notices ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->maybe_display_notice_about_product_ordering()" ); + return $this->products->maybe_display_notice_about_product_ordering( $notices ); + } + + /** + * DEPRECATED. Conditionally resync products after applying a custom order. + * + * @since 4.4.0 + * @param int $sorting_id ID of post dragged and dropped + * @param array $menu_orders Post IDs and their new menu_order value + */ + public function action_sync_on_woocommerce_sort_single( $sorting_id, $menu_orders ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->action_sync_on_woocommerce_sort_single()" ); + return $this->products->action_sync_on_woocommerce_sort_single( $sorting_id, $menu_orders ); + } + + /** + * DEPRECATED. Add weight by date settings related to WooCommerce * * @since 4.6.0 * @param array $settings Current settings. */ public function add_weight_settings_search( $settings ) { - ?> -
- - get_registered_feature( 'woocommerce' )->products->add_weight_settings_search()" ); + $this->products->add_weight_settings_search( $settings ); } /** - * Conditionally disable decaying by date based on WooCommerce Decay settings. + * DEPRECATED. Conditionally disable decaying by date based on WooCommerce Decay settings. * * @since 4.6.0 * @param bool $is_decaying_enabled Whether decay by date is enabled or not @@ -1401,25 +984,7 @@ public function add_weight_settings_search( $settings ) { * @return bool */ public function maybe_disable_decaying( $is_decaying_enabled, $settings, $args ) { - if ( ! in_array( $settings['decaying_enabled'], [ 'disabled_only_products', 'disabled_includes_products' ], true ) ) { - return $is_decaying_enabled; - } - - if ( ! isset( $args['post_type'] ) || ! in_array( 'product', (array) $args['post_type'], true ) ) { - return $is_decaying_enabled; - } - - $post_types = (array) $args['post_type']; - - if ( 'disabled_only_products' === $settings['decaying_enabled'] && count( $post_types ) > 1 ) { - return $is_decaying_enabled; - } - - if ( 'disabled_includes_products' === $settings['decaying_enabled'] && ! in_array( 'product', $post_types, true ) ) { - return $is_decaying_enabled; - } - - return false; + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->maybe_disable_decaying()" ); + return $this->products->maybe_disable_decaying( $is_decaying_enabled, $settings, $args ); } - } From 4fb7a08e585166c7fc3eed993c91415d3c2603a7 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Wed, 21 Jun 2023 15:53:16 -0300 Subject: [PATCH 02/11] Move and adjust tests and ->orders references --- .../Feature/WooCommerce/WooCommerce.php | 2 +- .../classes/StatusReport/ElasticPressIo.php | 2 +- .../{ => WooCommerce}/TestWooCommerce.php | 0 .../TestWooCommerceOrdersAutosuggest.php} | 40 +++++++++---------- 4 files changed, 22 insertions(+), 22 deletions(-) rename tests/php/features/{ => WooCommerce}/TestWooCommerce.php (100%) rename tests/php/features/{TestWooCommerceOrders.php => WooCommerce/TestWooCommerceOrdersAutosuggest.php} (81%) diff --git a/includes/classes/Feature/WooCommerce/WooCommerce.php b/includes/classes/Feature/WooCommerce/WooCommerce.php index 6ac5a7c68..3a1038d76 100644 --- a/includes/classes/Feature/WooCommerce/WooCommerce.php +++ b/includes/classes/Feature/WooCommerce/WooCommerce.php @@ -284,7 +284,7 @@ public function translate_args( $query ) { if ( ! empty( $s ) ) { - $searchable_post_types = $this->get_admin_searchable_post_types(); + $searchable_post_types = $this->orders->get_admin_searchable_post_types(); if ( in_array( $post_type, $searchable_post_types, true ) ) { $default_search_fields = array( 'post_title', 'post_content', 'post_excerpt' ); diff --git a/includes/classes/StatusReport/ElasticPressIo.php b/includes/classes/StatusReport/ElasticPressIo.php index 4ebd23695..31f67f609 100644 --- a/includes/classes/StatusReport/ElasticPressIo.php +++ b/includes/classes/StatusReport/ElasticPressIo.php @@ -248,7 +248,7 @@ protected function get_orders_search_field() : array { } $woocommerce_feature = \ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); - $template = $woocommerce_feature->orders->get_search_template(); + $template = $woocommerce_feature->orders_autosuggest->get_search_template(); if ( is_wp_error( $template ) ) { return [ diff --git a/tests/php/features/TestWooCommerce.php b/tests/php/features/WooCommerce/TestWooCommerce.php similarity index 100% rename from tests/php/features/TestWooCommerce.php rename to tests/php/features/WooCommerce/TestWooCommerce.php diff --git a/tests/php/features/TestWooCommerceOrders.php b/tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php similarity index 81% rename from tests/php/features/TestWooCommerceOrders.php rename to tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php index 1160d7812..1fc969c71 100644 --- a/tests/php/features/TestWooCommerceOrders.php +++ b/tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php @@ -13,7 +13,7 @@ /** * WC Orders test class */ -class TestWooCommerceOrders extends BaseTestCase { +class TestWooCommerceOrdersAutosuggest extends BaseTestCase { /** * Instance of the feature * @@ -24,14 +24,14 @@ class TestWooCommerceOrders extends BaseTestCase { /** * Orders instance * - * @var \ElasticPress\Feature\WooCommerce\Orders + * @var \ElasticPress\Feature\WooCommerce\OrdersAutosuggest */ - public $orders; + public $orders_autosuggest; /** * Setup each test. * - * @group WooCommerceOrders + * @group WooCommerceOrdersAutosuggest */ public function set_up() { parent::set_up(); @@ -45,13 +45,13 @@ public function set_up() { ElasticPress\Features::factory()->setup_features(); - $this->orders = $this->woocommerce_feature->orders; + $this->orders_autosuggest = $this->woocommerce_feature->orders_autosuggest; } /** * Test the `filter_term_suggest` method * - * @group WooCommerceOrders + * @group WooCommerceOrdersAutosuggest */ public function testFilterTermSuggest() { $order = [ @@ -70,7 +70,7 @@ public function testFilterTermSuggest() { ], ]; - $order_with_suggest = $this->orders->filter_term_suggest( $order ); + $order_with_suggest = $this->orders_autosuggest->filter_term_suggest( $order ); $this->assertArrayHasKey( 'term_suggest', $order_with_suggest ); $this->assertContains( '_billing_email_example', $order_with_suggest['term_suggest'] ); @@ -86,16 +86,16 @@ public function testFilterTermSuggest() { ); unset( $order['post_type'] ); - $order_with_suggest = $this->orders->filter_term_suggest( $order ); + $order_with_suggest = $this->orders_autosuggest->filter_term_suggest( $order ); $this->assertArrayNotHasKey( 'term_suggest', $order_with_suggest ); $order['post_type'] = 'not_shop_order'; - $order_with_suggest = $this->orders->filter_term_suggest( $order ); + $order_with_suggest = $this->orders_autosuggest->filter_term_suggest( $order ); $this->assertArrayNotHasKey( 'term_suggest', $order_with_suggest ); $order['post_type'] = 'shop_order'; unset( $order['meta'] ); - $order_with_suggest = $this->orders->filter_term_suggest( $order ); + $order_with_suggest = $this->orders_autosuggest->filter_term_suggest( $order ); $this->assertArrayNotHasKey( 'term_suggest', $order_with_suggest ); } @@ -104,7 +104,7 @@ public function testFilterTermSuggest() { * * This method steps into WooCommerce functionality a bit. * - * @group WooCommerceOrders + * @group WooCommerceOrdersAutosuggest */ public function testFilterTermSuggestWithCustomOrderId() { $shop_order_1 = new \WC_Order(); @@ -115,7 +115,7 @@ public function testFilterTermSuggestWithCustomOrderId() { $shop_order_id_1 = (string) $shop_order_1->get_id(); $prepared_shop_order = ElasticPress\Indexables::factory()->get( 'post' )->prepare_document( $shop_order_id_1 ); - $order_with_suggest = $this->orders->filter_term_suggest( $prepared_shop_order ); + $order_with_suggest = $this->orders_autosuggest->filter_term_suggest( $prepared_shop_order ); $this->assertSame( [ @@ -133,7 +133,7 @@ public function testFilterTermSuggestWithCustomOrderId() { }; add_filter( 'woocommerce_order_number', $set_custom_order_id ); - $order_with_suggest = $this->orders->filter_term_suggest( $prepared_shop_order ); + $order_with_suggest = $this->orders_autosuggest->filter_term_suggest( $prepared_shop_order ); $this->assertSame( [ @@ -147,7 +147,7 @@ public function testFilterTermSuggestWithCustomOrderId() { /** * Test the `mapping` method with the ES 7 mapping * - * @group WooCommerceOrders + * @group WooCommerceOrdersAutosuggest */ public function testMappingEs7() { $original_mapping = [ @@ -157,7 +157,7 @@ public function testMappingEs7() { ], ], ]; - $changed_mapping = $this->orders->mapping( $original_mapping ); + $changed_mapping = $this->orders_autosuggest->mapping( $original_mapping ); $expected_mapping = [ 'mappings' => [ @@ -192,7 +192,7 @@ public function testMappingEs7() { /** * Test the `mapping` method with the ES 5 mapping * - * @group WooCommerceOrders + * @group WooCommerceOrdersAutosuggest */ public function testMappingEs5() { $change_es_version = function() { @@ -210,7 +210,7 @@ public function testMappingEs5() { ], ]; - $changed_mapping = $this->orders->mapping( $original_mapping ); + $changed_mapping = $this->orders_autosuggest->mapping( $original_mapping ); $expected_mapping = [ 'mappings' => [ @@ -247,7 +247,7 @@ public function testMappingEs5() { /** * Test the `set_search_fields` method * - * @group WooCommerceOrders + * @group WooCommerceOrdersAutosuggest */ public function testSetSearchFields() { $original_search_fields = [ 'old_search_field' ]; @@ -261,7 +261,7 @@ public function testSetSearchFields() { ] ); - $changed_search_fields = $this->orders->set_search_fields( $original_search_fields, $wp_query ); + $changed_search_fields = $this->orders_autosuggest->set_search_fields( $original_search_fields, $wp_query ); $this->assertSame( $original_search_fields, $changed_search_fields ); @@ -274,7 +274,7 @@ public function testSetSearchFields() { ] ); - $changed_search_fields = $this->orders->set_search_fields( $original_search_fields, $wp_query ); + $changed_search_fields = $this->orders_autosuggest->set_search_fields( $original_search_fields, $wp_query ); $expected_fields = [ 'meta.order_number.value', From 57e6958a825f2b6e14b563ccd6b48fad51b6f4aa Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Wed, 21 Jun 2023 16:28:05 -0300 Subject: [PATCH 03/11] More tests fix --- .../features/WooCommerce/TestWooCommerce.php | 3 + .../WooCommerce/TestWooCommerceOrders.php | 89 +++++++++++++++++++ .../WooCommerce/TestWooCommerceProduct.php | 53 +++++++++++ 3 files changed, 145 insertions(+) create mode 100644 tests/php/features/WooCommerce/TestWooCommerceOrders.php create mode 100644 tests/php/features/WooCommerce/TestWooCommerceProduct.php diff --git a/tests/php/features/WooCommerce/TestWooCommerce.php b/tests/php/features/WooCommerce/TestWooCommerce.php index 8e5be3688..0304961a7 100644 --- a/tests/php/features/WooCommerce/TestWooCommerce.php +++ b/tests/php/features/WooCommerce/TestWooCommerce.php @@ -332,6 +332,7 @@ public function testSearchOnAllFrontEnd() { * * @since 4.2.0 * @group woocommerce + * @expectedDeprecated ElasticPress\Feature\WooCommerce\WooCommerce::add_variations_skus_meta */ public function testAddVariationsSkusMeta() { ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); @@ -369,6 +370,7 @@ public function testAddVariationsSkusMeta() { * * @since 4.2.0 * @group woocommerce + * @expectedDeprecated ElasticPress\Feature\WooCommerce\WooCommerce::translate_args_admin_products_list */ public function testTranslateArgsAdminProductsList() { ElasticPress\Features::factory()->activate_feature( 'protected_content' ); @@ -411,6 +413,7 @@ public function testTranslateArgsAdminProductsList() { * * @since 4.2.0 * @group woocommerce + * @expectedDeprecated ElasticPress\Feature\WooCommerce\WooCommerce::translate_args_admin_products_list */ public function testEPWoocommerceAdminProductsListSearchFields() { ElasticPress\Features::factory()->activate_feature( 'protected_content' ); diff --git a/tests/php/features/WooCommerce/TestWooCommerceOrders.php b/tests/php/features/WooCommerce/TestWooCommerceOrders.php new file mode 100644 index 000000000..73ad63f7b --- /dev/null +++ b/tests/php/features/WooCommerce/TestWooCommerceOrders.php @@ -0,0 +1,89 @@ +activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + parse_str( 'post_type=product&s=product&product_type=downloadable&stock_status=instock', $_GET ); + + $query_args = [ + 'ep_integrate' => true, + ]; + + $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); + add_action( 'pre_get_posts', [ $woocommerce_feature->orders, 'translate_args_admin_products_list' ] ); + + $query = new \WP_Query( $query_args ); + + $this->assertTrue( $query->elasticsearch_success ); + $this->assertEquals( $query->query_vars['s'], 'product' ); + $this->assertEquals( $query->query_vars['meta_query'][0]['key'], '_downloadable' ); + $this->assertEquals( $query->query_vars['meta_query'][0]['value'], 'yes' ); + $this->assertEquals( $query->query_vars['meta_query'][1]['key'], '_stock_status' ); + $this->assertEquals( $query->query_vars['meta_query'][1]['value'], 'instock' ); + $this->assertEquals( + $query->query_vars['search_fields'], + [ + 'post_title', + 'post_content', + 'post_excerpt', + 'meta' => [ + '_sku', + '_variations_skus', + ], + ] + ); + } + + /** + * Test the ep_woocommerce_admin_products_list_search_fields filter + * + * @group woocommerce + */ + public function testEPWoocommerceAdminProductsListSearchFields() { + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + parse_str( 'post_type=product&s=product&product_type=downloadable', $_GET ); + + $query_args = [ + 'ep_integrate' => true, + ]; + + $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); + add_action( 'pre_get_posts', [ $woocommerce_feature->orders, 'translate_args_admin_products_list' ] ); + + $search_fields_function = function () { + return [ 'post_title', 'post_content' ]; + }; + add_filter( 'ep_woocommerce_admin_products_list_search_fields', $search_fields_function ); + + $query = new \WP_Query( $query_args ); + $this->assertEquals( + $query->query_vars['search_fields'], + [ 'post_title', 'post_content' ] + ); + } +} diff --git a/tests/php/features/WooCommerce/TestWooCommerceProduct.php b/tests/php/features/WooCommerce/TestWooCommerceProduct.php new file mode 100644 index 000000000..54df9ed8f --- /dev/null +++ b/tests/php/features/WooCommerce/TestWooCommerceProduct.php @@ -0,0 +1,53 @@ +activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $this->assertTrue( class_exists( '\WC_Product_Variable' ) ); + $this->assertTrue( class_exists( '\WC_Product_Variation' ) ); + + $main_product = new \WC_Product_Variable(); + $main_product->set_sku( 'main-product_sku' ); + $main_product_id = $main_product->save(); + + $variation_1 = new \WC_Product_Variation(); + $variation_1->set_parent_id( $main_product_id ); + $variation_1->set_sku( 'child-sku-1' ); + $variation_1->save(); + + $variation_2 = new \WC_Product_Variation(); + $variation_2->set_parent_id( $main_product_id ); + $variation_2->set_sku( 'child-sku-2' ); + $variation_2->save(); + + $main_product_as_post = get_post( $main_product_id ); + $product_meta_to_index = ElasticPress\Features::factory() + ->get_registered_feature( 'woocommerce' ) + ->products + ->add_variations_skus_meta( [], $main_product_as_post ); + + $this->assertArrayHasKey( '_variations_skus', $product_meta_to_index ); + $this->assertContains( 'child-sku-1', $product_meta_to_index['_variations_skus'] ); + $this->assertContains( 'child-sku-2', $product_meta_to_index['_variations_skus'] ); + } +} From 56f9602b0a6b31403c6438db201108962dfa29c4 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Wed, 21 Jun 2023 16:34:14 -0300 Subject: [PATCH 04/11] More fixes --- .../WooCommerce/TestWooCommerceOrders.php | 71 ------------------ .../WooCommerce/TestWooCommerceProduct.php | 72 +++++++++++++++++++ 2 files changed, 72 insertions(+), 71 deletions(-) diff --git a/tests/php/features/WooCommerce/TestWooCommerceOrders.php b/tests/php/features/WooCommerce/TestWooCommerceOrders.php index 73ad63f7b..b29f74663 100644 --- a/tests/php/features/WooCommerce/TestWooCommerceOrders.php +++ b/tests/php/features/WooCommerce/TestWooCommerceOrders.php @@ -15,75 +15,4 @@ */ class TestWooCommerceOrders extends BaseTestCase { - /** - * Test the translate_args_admin_products_list method - * - * @group woocommerce - */ - public function testTranslateArgsAdminProductsList() { - ElasticPress\Features::factory()->activate_feature( 'protected_content' ); - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - parse_str( 'post_type=product&s=product&product_type=downloadable&stock_status=instock', $_GET ); - - $query_args = [ - 'ep_integrate' => true, - ]; - - $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); - add_action( 'pre_get_posts', [ $woocommerce_feature->orders, 'translate_args_admin_products_list' ] ); - - $query = new \WP_Query( $query_args ); - - $this->assertTrue( $query->elasticsearch_success ); - $this->assertEquals( $query->query_vars['s'], 'product' ); - $this->assertEquals( $query->query_vars['meta_query'][0]['key'], '_downloadable' ); - $this->assertEquals( $query->query_vars['meta_query'][0]['value'], 'yes' ); - $this->assertEquals( $query->query_vars['meta_query'][1]['key'], '_stock_status' ); - $this->assertEquals( $query->query_vars['meta_query'][1]['value'], 'instock' ); - $this->assertEquals( - $query->query_vars['search_fields'], - [ - 'post_title', - 'post_content', - 'post_excerpt', - 'meta' => [ - '_sku', - '_variations_skus', - ], - ] - ); - } - - /** - * Test the ep_woocommerce_admin_products_list_search_fields filter - * - * @group woocommerce - */ - public function testEPWoocommerceAdminProductsListSearchFields() { - ElasticPress\Features::factory()->activate_feature( 'protected_content' ); - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - parse_str( 'post_type=product&s=product&product_type=downloadable', $_GET ); - - $query_args = [ - 'ep_integrate' => true, - ]; - - $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); - add_action( 'pre_get_posts', [ $woocommerce_feature->orders, 'translate_args_admin_products_list' ] ); - - $search_fields_function = function () { - return [ 'post_title', 'post_content' ]; - }; - add_filter( 'ep_woocommerce_admin_products_list_search_fields', $search_fields_function ); - - $query = new \WP_Query( $query_args ); - $this->assertEquals( - $query->query_vars['search_fields'], - [ 'post_title', 'post_content' ] - ); - } } diff --git a/tests/php/features/WooCommerce/TestWooCommerceProduct.php b/tests/php/features/WooCommerce/TestWooCommerceProduct.php index 54df9ed8f..80e375d0d 100644 --- a/tests/php/features/WooCommerce/TestWooCommerceProduct.php +++ b/tests/php/features/WooCommerce/TestWooCommerceProduct.php @@ -50,4 +50,76 @@ public function testAddVariationsSkusMeta() { $this->assertContains( 'child-sku-1', $product_meta_to_index['_variations_skus'] ); $this->assertContains( 'child-sku-2', $product_meta_to_index['_variations_skus'] ); } + + /** + * Test the translate_args_admin_products_list method + * + * @group woocommerce + */ + public function testTranslateArgsAdminProductsList() { + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + parse_str( 'post_type=product&s=product&product_type=downloadable&stock_status=instock', $_GET ); + + $query_args = [ + 'ep_integrate' => true, + ]; + + $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); + add_action( 'pre_get_posts', [ $woocommerce_feature->products, 'translate_args_admin_products_list' ] ); + + $query = new \WP_Query( $query_args ); + + $this->assertTrue( $query->elasticsearch_success ); + $this->assertEquals( $query->query_vars['s'], 'product' ); + $this->assertEquals( $query->query_vars['meta_query'][0]['key'], '_downloadable' ); + $this->assertEquals( $query->query_vars['meta_query'][0]['value'], 'yes' ); + $this->assertEquals( $query->query_vars['meta_query'][1]['key'], '_stock_status' ); + $this->assertEquals( $query->query_vars['meta_query'][1]['value'], 'instock' ); + $this->assertEquals( + $query->query_vars['search_fields'], + [ + 'post_title', + 'post_content', + 'post_excerpt', + 'meta' => [ + '_sku', + '_variations_skus', + ], + ] + ); + } + + /** + * Test the ep_woocommerce_admin_products_list_search_fields filter + * + * @group woocommerce + */ + public function testEPWoocommerceAdminProductsListSearchFields() { + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + parse_str( 'post_type=product&s=product&product_type=downloadable', $_GET ); + + $query_args = [ + 'ep_integrate' => true, + ]; + + $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); + add_action( 'pre_get_posts', [ $woocommerce_feature->products, 'translate_args_admin_products_list' ] ); + + $search_fields_function = function () { + return [ 'post_title', 'post_content' ]; + }; + add_filter( 'ep_woocommerce_admin_products_list_search_fields', $search_fields_function ); + + $query = new \WP_Query( $query_args ); + $this->assertEquals( + $query->query_vars['search_fields'], + [ 'post_title', 'post_content' ] + ); + } } From 37bb1678643c4f9ad99d8d463dad90af56b4854e Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Wed, 21 Jun 2023 17:08:19 -0300 Subject: [PATCH 05/11] More tests --- .../classes/Feature/WooCommerce/Orders.php | 5 ++- .../features/WooCommerce/TestWooCommerce.php | 41 ------------------- .../WooCommerce/TestWooCommerceOrders.php | 40 +++++++++++++++++- 3 files changed, 42 insertions(+), 44 deletions(-) diff --git a/includes/classes/Feature/WooCommerce/Orders.php b/includes/classes/Feature/WooCommerce/Orders.php index d4cd0bf94..b2f2a1c6f 100644 --- a/includes/classes/Feature/WooCommerce/Orders.php +++ b/includes/classes/Feature/WooCommerce/Orders.php @@ -41,6 +41,7 @@ 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' ] ); 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 ); } @@ -224,12 +225,12 @@ public function maybe_hook_woocommerce_search_fields( $query ) { * @param WP_Query $wp WP Query */ public function search_order( $wp ) { + global $pagenow; + if ( ! $this->woocommerce->should_integrate_with_query( $wp ) ) { return; } - global $pagenow; - $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 ) || diff --git a/tests/php/features/WooCommerce/TestWooCommerce.php b/tests/php/features/WooCommerce/TestWooCommerce.php index 0304961a7..527248ea3 100644 --- a/tests/php/features/WooCommerce/TestWooCommerce.php +++ b/tests/php/features/WooCommerce/TestWooCommerce.php @@ -264,47 +264,6 @@ public function testSearchShopOrderById() { $this->assertEquals( 1, $query->found_posts ); } - /** - * Test search for shop orders matching field and ID. - * - * If searching for a number that is an order ID and part of another order's metadata, - * both should be returned. - * - * @since 4.0.0 - * @group woocommerce - */ - public function testSearchShopOrderByMetaFieldAndId() { - ElasticPress\Features::factory()->activate_feature( 'protected_content' ); - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $this->assertTrue( class_exists( '\WC_Order' ) ); - - $shop_order_1 = new \WC_Order(); - $shop_order_1->save(); - $shop_order_id_1 = $shop_order_1->get_id(); - ElasticPress\Indexables::factory()->get( 'post' )->index( $shop_order_id_1, true ); - - $shop_order_2 = new \WC_Order(); - $shop_order_2->set_billing_phone( 'Phone number that matches an order ID: ' . $shop_order_id_1 ); - $shop_order_2->save(); - ElasticPress\Indexables::factory()->get( 'post' )->index( $shop_order_2->get_id(), true ); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); - - $args = array( - 's' => (string) $shop_order_id_1, - 'post_type' => 'shop_order', - 'post_status' => 'any', - ); - - $query = new \WP_Query( $args ); - - $this->assertTrue( $query->elasticsearch_success ); - $this->assertEquals( 2, $query->post_count ); - $this->assertEquals( 2, $query->found_posts ); - } - /** * Test search integration is on in general for product searches * diff --git a/tests/php/features/WooCommerce/TestWooCommerceOrders.php b/tests/php/features/WooCommerce/TestWooCommerceOrders.php index b29f74663..b581a2a3c 100644 --- a/tests/php/features/WooCommerce/TestWooCommerceOrders.php +++ b/tests/php/features/WooCommerce/TestWooCommerceOrders.php @@ -13,6 +13,44 @@ /** * WC orders test class */ -class TestWooCommerceOrders extends BaseTestCase { +class TestWooCommerceOrders extends TestWooCommerce { + /** + * Test search for shop orders matching field and ID. + * + * If searching for a number that is an order ID and part of another order's metadata, + * both should be returned. + * + * @group woocommerce + */ + public function testSearchShopOrderByMetaFieldAndId() { + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + $this->assertTrue( class_exists( '\WC_Order' ) ); + + $shop_order_1 = new \WC_Order(); + $shop_order_1->save(); + $shop_order_id_1 = $shop_order_1->get_id(); + ElasticPress\Indexables::factory()->get( 'post' )->index( $shop_order_id_1, true ); + + $shop_order_2 = new \WC_Order(); + $shop_order_2->set_billing_phone( 'Phone number that matches an order ID: ' . $shop_order_id_1 ); + $shop_order_2->save(); + ElasticPress\Indexables::factory()->get( 'post' )->index( $shop_order_2->get_id(), true ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $args = array( + 's' => (string) $shop_order_id_1, + 'post_type' => 'shop_order', + 'post_status' => 'any', + ); + + $query = new \WP_Query( $args ); + + $this->assertTrue( $query->elasticsearch_success ); + $this->assertEquals( 2, $query->post_count ); + $this->assertEquals( 2, $query->found_posts ); + } } From 00a9132a8e6f8c9cd2952ec9c487c4ce43d7caac Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Thu, 22 Jun 2023 13:03:36 -0300 Subject: [PATCH 06/11] Split translate_args --- .../classes/Feature/WooCommerce/Orders.php | 151 +++++++ .../classes/Feature/WooCommerce/Products.php | 390 ++++++++++++++++ .../Feature/WooCommerce/WooCommerce.php | 425 ++---------------- 3 files changed, 584 insertions(+), 382 deletions(-) diff --git a/includes/classes/Feature/WooCommerce/Orders.php b/includes/classes/Feature/WooCommerce/Orders.php index b2f2a1c6f..9d127f965 100644 --- a/includes/classes/Feature/WooCommerce/Orders.php +++ b/includes/classes/Feature/WooCommerce/Orders.php @@ -44,6 +44,7 @@ public function setup() { 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 ); } /** @@ -246,4 +247,154 @@ public function search_order( $wp ) { } // 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' ]; + + /** + * 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( 'ep_woocommerce_default_supported_post_types', $post_types ); + + $supported_post_types = array_intersect( + $supported_post_types, + Indexables::factory()->get( 'post' )->get_indexable_post_types() + ); + + return $supported_post_types; + } + + /** + * 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 + */ + public 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 ); + } } diff --git a/includes/classes/Feature/WooCommerce/Products.php b/includes/classes/Feature/WooCommerce/Products.php index b94179dc8..d8a480de6 100644 --- a/includes/classes/Feature/WooCommerce/Products.php +++ b/includes/classes/Feature/WooCommerce/Products.php @@ -50,6 +50,8 @@ public function setup() { add_filter( 'ep_prepare_meta_data', [ $this, 'add_variations_skus_meta' ], 10, 2 ); add_filter( 'request', [ $this, 'admin_product_list_request_query' ], 9 ); + add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 ); + // Custom product ordering add_action( 'ep_admin_notices', [ $this, 'maybe_display_notice_about_product_ordering' ] ); add_action( 'woocommerce_after_product_ordering', [ $this, 'action_sync_on_woocommerce_sort_single' ], 10, 2 ); @@ -582,4 +584,392 @@ public function maybe_disable_decaying( $is_decaying_enabled, $settings, $args ) return false; } + + /** + * 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; + } + + /** + * Make sure filters are suppressed + */ + $query->query['suppress_filters'] = false; + $query->set( 'suppress_filters', false ); + + $query->set( 'ep_integrate', true ); + + $this->maybe_update_tax_query( $query ); + $this->maybe_update_post_type( $query ); + $this->maybe_update_meta_query( $query ); + + $this->maybe_handle_top_rated( $query ); + + $this->maybe_set_search_fields( $query ); + $this->maybe_set_orderby( $query ); + } + + /** + * 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 for taxonomies + */ + $supported_taxonomies = $this->get_supported_taxonomies(); + $tax_query = $query->get( 'tax_query', [] ); + $taxonomies_queried = array_merge( + array_column( $tax_query, 'taxonomy' ), + array_keys( $query->query_vars ) + ); + if ( ! empty( array_intersect( $supported_taxonomies, $taxonomies_queried ) ) ) { + return true; + } + + /** + * 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 WooCommerce supported taxonomies (related to products.) + * + * @return array + */ + public function get_supported_taxonomies() : array { + $supported_taxonomies = array( + 'product_cat', + 'product_tag', + 'product_type', + 'product_visibility', + 'product_shipping_class', + ); + + // Add in any attribute taxonomies that exist + $attribute_taxonomies = wc_get_attribute_taxonomy_names(); + + $supported_taxonomies = array_merge( $supported_taxonomies, $attribute_taxonomies ); + + /** + * DEPRECATED. Filter supported custom taxonomies for WooCommerce integration. + * + * @param {array} $supported_taxonomies An array of default taxonomies. + * @hook ep_woocommerce_supported_taxonomies + * @since 2.3.0 + * @return {array} New taxonomies + */ + $supported_taxonomies = apply_filters_deprecated( + 'ep_woocommerce_supported_taxonomies', + [ $supported_taxonomies ], + '4.7.0', + 'ep_woocommerce_products_supported_taxonomies' + ); + + /** + * Filter supported custom taxonomies for WooCommerce product queries integration + * + * @param {array} $supported_taxonomies An array of default taxonomies. + * @hook ep_woocommerce_products_supported_taxonomies + * @since 4.7.0 + * @return {array} New taxonomies + */ + return apply_filters( 'ep_woocommerce_products_supported_taxonomies', $supported_taxonomies ); + } + + /** + * Get the WooCommerce supported post types (related to products.) + * + * @param \WP_Query $query The WP_Query object + * @return array + */ + public function get_supported_post_types( \WP_Query $query ) : array { + $post_types = [ 'product_variation' ]; + + $is_main_post_type_archive = $query->is_main_query() && $query->is_post_type_archive( 'product' ); + $has_ep_integrate_set_true = isset( $query->query_vars['ep_integrate'] ) && filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN ); + if ( $is_main_post_type_archive || $has_ep_integrate_set_true ) { + $post_types[] = 'product'; + } + + /** + * 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( 'ep_woocommerce_default_supported_post_types', $post_types ); + + $supported_post_types = array_intersect( + $supported_post_types, + Indexables::factory()->get( 'post' )->get_indexable_post_types() + ); + + return $supported_post_types; + } + + /** + * If needed, update the `'tax_query'` parameter + * + * If a supported taxonomy was added in the root of the args array, + * this method moves it to the `'tax_query'` + * + * @param \WP_Query $query The WP_Query object + */ + protected function maybe_update_tax_query( \WP_Query $query ) { + $supported_taxonomies = $this->get_supported_taxonomies(); + $tax_query = $query->get( 'tax_query', [] ); + + foreach ( $supported_taxonomies as $taxonomy ) { + $term = $query->get( $taxonomy, false ); + + if ( ! empty( $term ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'slug', + 'terms' => (array) $term, + ); + } + } + + $query->set( 'tax_query', $tax_query ); + } + + /** + * Set the post_type to product if empty + * + * @param \WP_Query $query The WP_Query object + */ + protected function maybe_update_post_type( \WP_Query $query ) { + $post_type = $query->get( 'post_type', false ); + + if ( empty( $post_type ) ) { + $query->set( 'post_type', 'product' ); + } + } + + /** + * If the `'meta_key'` or `'meta_value'` parameters were set, + * move them to `'meta_query'` + * + * @param \WP_Query $query The WP_Query object + */ + protected function maybe_update_meta_query( \WP_Query $query ) { + /** + * Handle meta queries + */ + $meta_query = $query->get( 'meta_query', [] ); + $meta_key = $query->get( 'meta_key', false ); + $meta_value = $query->get( 'meta_value', false ); + + if ( ! empty( $meta_key ) && ! empty( $meta_value ) ) { + $meta_query[] = array( + 'key' => $meta_key, + 'value' => $meta_value, + ); + + $query->set( 'meta_query', $meta_query ); + } + } + + /** + * Handle the WC Top Rated Widget + * + * @param \WP_Query $query The WP_Query object + * @return void + */ + protected function maybe_handle_top_rated( \WP_Query $query ) { + if ( ! has_filter( 'posts_clauses', array( WC()->query, 'order_by_rating_post_clauses' ) ) ) { + return; + } + + remove_filter( 'posts_clauses', array( WC()->query, 'order_by_rating_post_clauses' ) ); + $query->set( 'orderby', 'meta_value_num' ); + $query->set( 'meta_key', '_wc_average_rating' ); + } + + /** + * If the query has a search term and the weighting dashboard is not + * available, add the needed fields + * + * @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; + } + + $post_type = $query->get( 'post_type', false ); + if ( 'product' !== $post_type || ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) { + return; + } + + $search_fields = $query->get( 'search_fields', array( 'post_title', 'post_content', 'post_excerpt' ) ); + + // Remove author_name from this search. + $search_fields = $this->remove_author( $search_fields ); + + $search_fields['meta'] = ( ! empty( $search_fields['meta'] ) ) ? $search_fields['meta'] : []; + $search_fields['taxonomies'] = ( ! empty( $search_fields['taxonomies'] ) ) ? $search_fields['taxonomies'] : []; + + $search_fields['meta'] = array_merge( $search_fields['meta'], array( '_sku' ) ); + $search_fields['taxonomies'] = array_merge( $search_fields['taxonomies'], array( 'category', 'post_tag', 'product_tag', 'product_cat' ) ); + + $query->set( 'search_fields', $search_fields ); + } + + /** + * Remove the author_name from search fields. + * + * @param array $search_fields Array of search fields. + * @return array + */ + public function remove_author( array $search_fields ) : array { + foreach ( $search_fields as $field_key => $field ) { + if ( 'author_name' === $field ) { + unset( $search_fields[ $field_key ] ); + } + } + + return $search_fields; + } + + /** + * If needed, set the `'order'` and `'orderby'` parameters + * + * @param \WP_Query $query The WP_Query object + */ + protected function maybe_set_orderby( \WP_Query $query ) { + $search_term = $this->woocommerce->get_search_term( $query ); + + if ( empty( $search_term ) ) { + /** + * For default sorting by popularity (total_sales) and rating + * Woocommerce doesn't set the orderby correctly. + * These lines will check the meta_key and correct the orderby based on that. + * And this won't run in search result and only run in main query + */ + $meta_key = $query->get( 'meta_key', false ); + if ( $meta_key && $query->is_main_query() ) { + switch ( $meta_key ) { + case 'total_sales': + $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) ); + $query->set( 'order', 'DESC' ); + break; + case '_wc_average_rating': + $query->set( 'orderby', $this->get_orderby_meta_mapping( '_wc_average_rating' ) ); + $query->set( 'order', 'DESC' ); + break; + } + } + } + + /** + * Set orderby and order for price/popularity when GET param not set + */ + if ( isset( $query->query_vars['orderby'], $query->query_vars['order'] ) && $query->is_main_query() ) { + switch ( $query->query_vars['orderby'] ) { + case 'price': + $query->set( 'order', $query->query_vars['order'] ); + $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) ); + break; + case 'popularity': + $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) ); + $query->set( 'order', 'DESC' ); + break; + } + } + + /** + * Set orderby from GET param + * Also make sure the orderby param affects only the main query + */ + if ( ! empty( $_GET['orderby'] ) && $query->is_main_query() ) { // phpcs:ignore WordPress.Security.NonceVerification + $orderby = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); // phpcs:ignore WordPress.Security.NonceVerification + switch ( $orderby ) { // phpcs:ignore WordPress.Security.NonceVerification + case 'popularity': + $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) ); + $query->set( 'order', 'DESC' ); + break; + case 'price': + $query->set( 'order', $query->get( 'order', 'ASC' ) ); + $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) ); + break; + case 'price-desc': + $query->set( 'order', 'DESC' ); + $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) ); + break; + case 'rating': + $query->set( 'orderby', $this->get_orderby_meta_mapping( '_wc_average_rating' ) ); + $query->set( 'order', 'DESC' ); + break; + case 'date': + case 'title': + case 'ID': + $query->set( 'orderby', $this->get_orderby_meta_mapping( $orderby ) ); + break; + case 'sku': + $query->set( 'orderby', $this->get_orderby_meta_mapping( '_sku' ) ); + break; + default: + $query->set( 'orderby', $this->get_orderby_meta_mapping( 'menu_order' ) ); // Order by menu and title. + } + } + } + + /** + * Fetch the ES related meta mapping for orderby + * + * @param array $meta_key The meta key to get the mapping for. + * @return string The mapped meta key. + */ + public function get_orderby_meta_mapping( $meta_key ) : string { + /** + * Filter WooCommerce to Elasticsearch meta mapping + * + * @hook orderby_meta_mapping + * @param {array} $mapping Meta mapping + * @return {array} New mapping + */ + $mapping = apply_filters( + 'orderby_meta_mapping', + array( + 'ID' => 'ID', + 'title' => 'title date', + 'menu_order' => 'menu_order title date', + 'menu_order title' => 'menu_order title date', + 'total_sales' => 'meta.total_sales.double date', + '_wc_average_rating' => 'meta._wc_average_rating.double date', + '_price' => 'meta._price.double date', + '_sku' => 'meta._sku.value.sortable date', + ) + ); + + if ( isset( $mapping[ $meta_key ] ) ) { + return $mapping[ $meta_key ]; + } + + return 'date'; + } } diff --git a/includes/classes/Feature/WooCommerce/WooCommerce.php b/includes/classes/Feature/WooCommerce/WooCommerce.php index 3a1038d76..25cb8868e 100644 --- a/includes/classes/Feature/WooCommerce/WooCommerce.php +++ b/includes/classes/Feature/WooCommerce/WooCommerce.php @@ -97,8 +97,6 @@ public function setup() { add_filter( 'woocommerce_unfiltered_product_ids', [ $this, 'convert_post_object_to_id' ], 10, 4 ); add_action( 'ep_wp_query_search_cached_posts', [ $this, 'disallow_duplicated_query' ], 10, 2 ); - add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 ); - // Orders Autosuggest feature. if ( $this->is_orders_autosuggest_enabled() ) { $this->orders_autosuggest->setup(); @@ -106,372 +104,17 @@ public function setup() { } /** - * Translate args to ElasticPress compat format. This is the meat of what the feature does + * Given a WP_Query object, return its search term (if any) * - * @param WP_Query $query WP Query - * @since 2.1 - */ - public function translate_args( $query ) { - if ( ! $this->should_integrate_with_query( $query ) ) { - return; - } - - // Flag to check and make sure we are in a WooCommerce specific query - $integrate = false; - - /** - * Force ElasticPress if we are querying WC taxonomy - */ - $tax_query = $query->get( 'tax_query', [] ); - - $supported_taxonomies = array( - 'product_cat', - 'product_tag', - 'product_type', - 'product_visibility', - 'product_shipping_class', - ); - - // Add in any attribute taxonomies that exist - $attribute_taxonomies = wc_get_attribute_taxonomy_names(); - - $supported_taxonomies = array_merge( $supported_taxonomies, $attribute_taxonomies ); - - /** - * Filter supported custom taxonomies for WooCommerce integration - * - * @param {array} $supported_taxonomies An array of default taxonomies. - * @hook ep_woocommerce_supported_taxonomies - * @since 2.3.0 - * @return {array} New taxonomies - */ - $supported_taxonomies = apply_filters( 'ep_woocommerce_supported_taxonomies', $supported_taxonomies ); - - if ( ! empty( $tax_query ) ) { - - /** - * First check if already set taxonomies are supported WC taxes - */ - foreach ( $tax_query as $taxonomy_array ) { - if ( isset( $taxonomy_array['taxonomy'] ) && in_array( $taxonomy_array['taxonomy'], $supported_taxonomies, true ) ) { - $integrate = true; - } - } - } - - /** - * Next check if any taxonomies are in the root of query vars (shorthand form) - */ - foreach ( $supported_taxonomies as $taxonomy ) { - $term = $query->get( $taxonomy, false ); - - if ( ! empty( $term ) ) { - $integrate = true; - - $tax_query[] = array( - 'taxonomy' => $taxonomy, - 'field' => 'slug', - 'terms' => (array) $term, - ); - } - } - - /** - * Force ElasticPress if product post type query - */ - $post_type = $query->get( 'post_type', false ); - - // Act only on a defined subset of all indexable post types here - $post_types = array( - 'shop_order', - 'shop_order_refund', - 'product_variation', - ); - - $is_main_post_type_archive = $query->is_main_query() && $query->is_post_type_archive( 'product' ); - $has_ep_integrate_set_true = isset( $query->query_vars['ep_integrate'] ) && filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN ); - if ( $is_main_post_type_archive || $has_ep_integrate_set_true ) { - $post_types[] = 'product'; - } - - /** - * 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( 'ep_woocommerce_default_supported_post_types', $post_types ); - - $supported_post_types = array_intersect( - $supported_post_types, - Indexables::factory()->get( 'post' )->get_indexable_post_types() - ); - - // For orders it queries an array of shop_order and shop_order_refund post types, hence an array_diff - if ( ! empty( $post_type ) && ( in_array( $post_type, $supported_post_types, true ) || ( is_array( $post_type ) && ! array_diff( $post_type, $supported_post_types ) ) ) ) { - $integrate = true; - } - - /** - * If we have a WooCommerce specific query, lets hook it to ElasticPress and make the query ElasticSearch friendly - */ - if ( ! $integrate ) { - return; - } - - // Set tax_query again since we may have added things - $query->set( 'tax_query', $tax_query ); - - // Default to product if no post type is set - if ( empty( $post_type ) ) { - $post_type = 'product'; - $query->set( 'post_type', 'product' ); - } - - // Handles the WC Top Rated Widget - if ( has_filter( 'posts_clauses', array( WC()->query, 'order_by_rating_post_clauses' ) ) ) { - remove_filter( 'posts_clauses', array( WC()->query, 'order_by_rating_post_clauses' ) ); - $query->set( 'orderby', 'meta_value_num' ); - $query->set( 'meta_key', '_wc_average_rating' ); - } - - /** - * WordPress have to be version 4.6 or newer to have "fields" support - * since it requires the "posts_pre_query" filter. - * - * @see WP_Query::get_posts - */ - $fields = $query->get( 'fields', false ); - if ( ! version_compare( get_bloginfo( 'version' ), '4.6', '>=' ) && ( 'ids' === $fields || 'id=>parent' === $fields ) ) { - $query->set( 'fields', 'default' ); - } - - /** - * Handle meta queries - */ - $meta_query = $query->get( 'meta_query', [] ); - $meta_key = $query->get( 'meta_key', false ); - $meta_value = $query->get( 'meta_value', false ); - - if ( ! empty( $meta_key ) && ! empty( $meta_value ) ) { - $meta_query[] = array( - 'key' => $meta_key, - 'value' => $meta_value, - ); - - $query->set( 'meta_query', $meta_query ); - } - - /** - * Make sure filters are suppressed - */ - $query->query['suppress_filters'] = false; - $query->set( 'suppress_filters', false ); - - // Integrate with WooCommerce custom searches as well - $search = $query->get( 'search' ); - if ( ! empty( $search ) ) { - $s = $search; - $query->set( 's', $s ); - } else { - $s = $query->get( 's' ); - } - - $query->query_vars['ep_integrate'] = true; - $query->query['ep_integrate'] = true; - - if ( ! empty( $s ) ) { - - $searchable_post_types = $this->orders->get_admin_searchable_post_types(); - - if ( in_array( $post_type, $searchable_post_types, true ) ) { - $default_search_fields = array( 'post_title', 'post_content', 'post_excerpt' ); - if ( ctype_digit( $s ) ) { - $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 ) - ); - } elseif ( 'product' === $post_type && defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) { - $search_fields = $query->get( 'search_fields', array( 'post_title', 'post_content', 'post_excerpt' ) ); - - // Remove author_name from this search. - $search_fields = $this->remove_author( $search_fields ); - - foreach ( $search_fields as $field_key => $field ) { - if ( 'author_name' === $field ) { - unset( $search_fields[ $field_key ] ); - } - } - - $search_fields['meta'] = ( ! empty( $search_fields['meta'] ) ) ? $search_fields['meta'] : []; - $search_fields['taxonomies'] = ( ! empty( $search_fields['taxonomies'] ) ) ? $search_fields['taxonomies'] : []; - - $search_fields['meta'] = array_merge( $search_fields['meta'], array( '_sku' ) ); - $search_fields['taxonomies'] = array_merge( $search_fields['taxonomies'], array( 'category', 'post_tag', 'product_tag', 'product_cat' ) ); - - $query->set( 'search_fields', $search_fields ); - } - } else { - /** - * For default sorting by popularity (total_sales) and rating - * Woocommerce doesn't set the orderby correctly. - * These lines will check the meta_key and correct the orderby based on that. - * And this won't run in search result and only run in main query - */ - $meta_key = $query->get( 'meta_key', false ); - if ( $meta_key && $query->is_main_query() ) { - switch ( $meta_key ) { - case 'total_sales': - $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) ); - $query->set( 'order', 'DESC' ); - break; - case '_wc_average_rating': - $query->set( 'orderby', $this->get_orderby_meta_mapping( '_wc_average_rating' ) ); - $query->set( 'order', 'DESC' ); - break; - } - } - } - - /** - * Set orderby and order for price/popularity when GET param not set - */ - if ( isset( $query->query_vars['orderby'], $query->query_vars['order'] ) && $query->is_main_query() ) { - switch ( $query->query_vars['orderby'] ) { - case 'price': - $query->set( 'order', $query->query_vars['order'] ); - $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) ); - break; - case 'popularity': - $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) ); - $query->set( 'order', 'DESC' ); - break; - } - } - - /** - * Set orderby from GET param - * Also make sure the orderby param affects only the main query - */ - if ( ! empty( $_GET['orderby'] ) && $query->is_main_query() ) { // phpcs:ignore WordPress.Security.NonceVerification - $orderby = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); // phpcs:ignore WordPress.Security.NonceVerification - switch ( $orderby ) { // phpcs:ignore WordPress.Security.NonceVerification - case 'popularity': - $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) ); - $query->set( 'order', 'DESC' ); - break; - case 'price': - $query->set( 'order', $query->get( 'order', 'ASC' ) ); - $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) ); - break; - case 'price-desc': - $query->set( 'order', 'DESC' ); - $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) ); - break; - case 'rating': - $query->set( 'orderby', $this->get_orderby_meta_mapping( '_wc_average_rating' ) ); - $query->set( 'order', 'DESC' ); - break; - case 'date': - case 'title': - case 'ID': - $query->set( 'orderby', $this->get_orderby_meta_mapping( $orderby ) ); - break; - case 'sku': - $query->set( 'orderby', $this->get_orderby_meta_mapping( '_sku' ) ); - break; - default: - $query->set( 'orderby', $this->get_orderby_meta_mapping( 'menu_order' ) ); // Order by menu and title. - } - } - } - - /** - * Fetch the ES related meta mapping for orderby + * This method also accounts for the `'search'` parameter used by + * WooCommerce, in addition to the regular `'s'` parameter. * - * @param array $meta_key The meta key to get the mapping for. - * @since 2.1 - * @return string The mapped meta key. + * @param \WP_Query $query The WP_Query object + * @return string */ - public function get_orderby_meta_mapping( $meta_key ) { - /** - * Filter WooCommerce to Elasticsearch meta mapping - * - * @hook orderby_meta_mapping - * @param {array} $mapping Meta mapping - * @return {array} New mapping - */ - $mapping = apply_filters( - 'orderby_meta_mapping', - array( - 'ID' => 'ID', - 'title' => 'title date', - 'menu_order' => 'menu_order title date', - 'menu_order title' => 'menu_order title date', - 'total_sales' => 'meta.total_sales.double date', - '_wc_average_rating' => 'meta._wc_average_rating.double date', - '_price' => 'meta._price.double date', - '_sku' => 'meta._sku.value.sortable date', - ) - ); - - if ( isset( $mapping[ $meta_key ] ) ) { - return $mapping[ $meta_key ]; - } - - return 'date'; + public function get_search_term( \WP_Query $query ) : string { + $search = $query->get( 'search' ); + return ( ! empty( $search ) ) ? $search : $query->get( 's', '' ); } /** @@ -543,23 +186,6 @@ public function output_feature_box_settings() { $field ) { - if ( 'author_name' === $field ) { - unset( $search_fields[ $field_key ] ); - } - } - - return $search_fields; - } - /** * Determine WC feature reqs status * @@ -668,6 +294,41 @@ public function is_orders_autosuggest_enabled() : bool { return $this->is_orders_autosuggest_available() && '1' === $this->get_setting( 'orders' ); } + /** + * DEPRECATED. Translate args to ElasticPress compat format. This is the meat of what the feature does + * + * @param \WP_Query $query WP Query + * @since 2.1 + */ + public function translate_args( $query ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->translate_args() OR \ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders->translate_args()" ); + $this->products->translate_args( $query ); + $this->orders->translate_args( $query ); + } + + /** + * DEPRECATED. Fetch the ES related meta mapping for orderby + * + * @param array $meta_key The meta key to get the mapping for. + * @since 2.1 + * @return string The mapped meta key. + */ + public function get_orderby_meta_mapping( $meta_key ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->get_orderby_meta_mapping()" ); + return $this->products->get_orderby_meta_mapping( $meta_key ); + } + + /** + * DEPRECATED. Remove the author_name from search fields. + * + * @param array $search_fields Array of search fields. + * @since 3.0 + * @return array + */ + public function remove_author( $search_fields ) { + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->remove_author()" ); + return $this->products->remove_author( $search_fields ); + } /** * DEPRECATED. Index Woocommerce meta From 145bcbaeb1a3ec3e388dd1e84c1c755fcd722222 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Thu, 22 Jun 2023 13:37:13 -0300 Subject: [PATCH 07/11] Move tests to their new places --- .../classes/Feature/WooCommerce/Orders.php | 46 + .../Feature/WooCommerce/OrdersAutosuggest.php | 7 + .../features/WooCommerce/TestWooCommerce.php | 1075 ++--------------- .../WooCommerce/TestWooCommerceOrders.php | 160 +++ .../WooCommerce/TestWooCommerceProduct.php | 769 +++++++++++- 5 files changed, 1081 insertions(+), 976 deletions(-) diff --git a/includes/classes/Feature/WooCommerce/Orders.php b/includes/classes/Feature/WooCommerce/Orders.php index 9d127f965..499546c48 100644 --- a/includes/classes/Feature/WooCommerce/Orders.php +++ b/includes/classes/Feature/WooCommerce/Orders.php @@ -397,4 +397,50 @@ public function translate_args( $query ) { $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( + __METHOD__, + '4.7.0', + "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders_autosuggest->{$method_name}()" // phpcs:ignore + ); + + if ( $this->woocommerce->is_orders_autosuggest_enabled() && method_exists( $this->woocommerce->orders_autosuggest, $method_name ) ) { + call_user_func_array( [ $this->woocommerce->orders_autosuggest, $method_name ], $arguments ); + } + } + } } diff --git a/includes/classes/Feature/WooCommerce/OrdersAutosuggest.php b/includes/classes/Feature/WooCommerce/OrdersAutosuggest.php index c68766da4..d2543e936 100644 --- a/includes/classes/Feature/WooCommerce/OrdersAutosuggest.php +++ b/includes/classes/Feature/WooCommerce/OrdersAutosuggest.php @@ -20,6 +20,13 @@ * WooCommerce OrdersAutosuggest Feature */ class OrdersAutosuggest { + /** + * The name of the index. + * + * @var string + */ + protected $index; + /** * Initialize feature. * diff --git a/tests/php/features/WooCommerce/TestWooCommerce.php b/tests/php/features/WooCommerce/TestWooCommerce.php index 527248ea3..886f90ba6 100644 --- a/tests/php/features/WooCommerce/TestWooCommerce.php +++ b/tests/php/features/WooCommerce/TestWooCommerce.php @@ -50,240 +50,195 @@ public function tear_down() { } /** - * Test products post type query does not get integrated when the feature is active + * Test search integration is on in general for product searches * * @since 2.1 * @group woocommerce */ - public function testProductsPostTypeQueryOn() { + public function testSearchOnAllFrontEnd() { ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); ElasticPress\Features::factory()->setup_features(); - $this->ep_factory->post->create(); - $this->ep_factory->product->create( - array( - 'description' => 'product 1', - ) - ); - ElasticPress\Elasticsearch::factory()->refresh_indices(); $args = array( + 's' => 'findme', 'post_type' => 'product', ); $query = new \WP_Query( $args ); - $this->assertNull( $query->elasticsearch_success ); - $this->assertEquals( 1, $query->post_count ); - $this->assertEquals( 1, $query->found_posts ); - } - - /** - * Test products post type query does get integrated when querying WC product_cat taxonomy - * - * @since 2.1 - * @group woocommerce - */ - public function testProductsPostTypeQueryProductCatTax() { - ElasticPress\Features::factory()->activate_feature( 'admin' ); - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $this->ep_factory->post->create(); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); - - $args = array( - 'tax_query' => array( - array( - 'taxonomy' => 'product_cat', - 'terms' => array( 'cat' ), - 'field' => 'slug', - ), - ), - ); - - $query = new \WP_Query( $args ); - $this->assertTrue( $query->elasticsearch_success ); } /** - * Test search integration is on for shop orders + * Tests the search query for a shop_coupon. * - * @since 2.1 + * @since 4.4.1 * @group woocommerce */ - public function testSearchOnShopOrderAdmin() { - ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + public function testSearchQueryForCoupon() { + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); ElasticPress\Features::factory()->setup_features(); - $this->ep_factory->post->create( - array( - 'post_content' => 'findme', - 'post_type' => 'shop_order', - ) + // ensures that the search query doesn't use Elasticsearch. + $query = new \WP_Query( + [ + 'post_type' => 'shop_coupon', + 's' => 'test-coupon', + ] ); + $this->assertNull( $query->elasticsearch_success ); - ElasticPress\Elasticsearch::factory()->refresh_indices(); - - // mock the pagenow to bypass the search_order checks - global $pagenow; - $pagenow = 'edit.php'; - - parse_str( 's=findme', $_GET ); - $args = array( - 's' => 'findme', - 'post_type' => 'shop_order', + // ensures that the search query doesn't use Elasticsearch when ep_integrate set to false. + $query = new \WP_Query( + [ + 'post_type' => 'shop_coupon', + 's' => 'test-coupon', + 'ep_integrate' => false, + ] ); + $this->assertNull( $query->elasticsearch_success ); - $query = new \WP_Query( $args ); - + // ensures that the search query use Elasticsearch when ep_integrate set to true. + $query = new \WP_Query( + [ + 'post_type' => 'shop_coupon', + 's' => 'test-coupon', + 'ep_integrate' => true, + ] + ); $this->assertTrue( $query->elasticsearch_success ); - $this->assertEquals( 1, $query->post_count ); - $this->assertEquals( 1, $query->found_posts ); - - $pagenow = 'index.php'; } /** - * Test Shop Order post type query does not get integrated when the protected content feature is deactivated. + * Tests the search query for a shop_coupon in admin use Elasticsearch when protected content is enabled. * - * @since 4.5 + * @since 4.4.1 + * @group woocommerce */ - public function testShopOrderPostTypeQueryOn() { - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $this->ep_factory->post->create(); - $this->ep_factory->post->create( - array( - 'post_type' => 'shop_order', - ) - ); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); - - $args = array( - 'post_type' => 'shop_order', - ); - $query = new \WP_Query( $args ); + public function testSearchQueryForCouponWhenProtectedContentIsEnable() { - $this->assertNull( $query->elasticsearch_success ); - $this->assertEquals( 1, $query->post_count ); - $this->assertEquals( 1, $query->found_posts ); - } + set_current_screen( 'dashboard' ); + $this->assertTrue( is_admin() ); - /** - * Test Shop Order post type query does get integrated when the protected content feature is activated. - * - * @since 4.5 - */ - public function testShopOrderPostTypeQueryWhenProtectedContentEnable() { ElasticPress\Features::factory()->activate_feature( 'protected_content' ); ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); ElasticPress\Features::factory()->setup_features(); - $this->ep_factory->post->create(); $this->ep_factory->post->create( array( - 'post_type' => 'shop_order', + 'post_content' => 'test-coupon', + 'post_type' => 'shop_coupon', ) ); ElasticPress\Elasticsearch::factory()->refresh_indices(); - $args = array( - 'post_type' => 'shop_order', + $query = new \WP_Query( + [ + 'post_type' => 'shop_coupon', + 's' => 'test-coupon', + ] ); - $query = new \WP_Query( $args ); $this->assertTrue( $query->elasticsearch_success ); $this->assertEquals( 1, $query->post_count ); - $this->assertEquals( 1, $query->found_posts ); } /** - * Test Shop Order post type query does not get integrated when the protected content feature is activated and ep_integrate is set to false. + * Tests the search query for a shop_coupon in admin does not use Elasticsearch when protected content is not enabled. * - * @since 4.5 + * @since 4.4.1 + * @group woocommerce */ - public function testShopOrderPostTypeQueryWhenEPIntegrateSetFalse() { - ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + public function testSearchQueryForCouponWhenProtectedContentIsNotEnable() { + + set_current_screen( 'dashboard' ); + $this->assertTrue( is_admin() ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); ElasticPress\Features::factory()->setup_features(); - $this->ep_factory->post->create(); $this->ep_factory->post->create( array( - 'post_type' => 'shop_order', + 'post_content' => 'test-coupon', + 'post_type' => 'shop_coupon', ) ); ElasticPress\Elasticsearch::factory()->refresh_indices(); - $args = array( - 'post_type' => 'shop_order', - 'ep_integrate' => false, + $query = new \WP_Query( + [ + 'post_type' => 'shop_coupon', + 's' => 'test-coupon', + 'ep_integrate' => true, + ] ); - $query = new \WP_Query( $args ); $this->assertNull( $query->elasticsearch_success ); + $this->assertEquals( 1, $query->post_count ); } /** - * Test search for shop orders by order ID + * Test the `is_orders_autosuggest_available` method * - * @since 4.0.0 + * @since 4.5.0 * @group woocommerce */ - public function testSearchShopOrderById() { - ElasticPress\Features::factory()->activate_feature( 'protected_content' ); - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $shop_order_id = $this->ep_factory->post->create( - array( - 'post_type' => 'shop_order', - ) - ); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); + public function testIsOrdersAutosuggestAvailable() { + $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); - $args = array( - 's' => (string) $shop_order_id, - 'post_type' => 'shop_order', - ); + $this->assertSame( $woocommerce_feature->is_orders_autosuggest_available(), \ElasticPress\Utils\is_epio() ); - $query = new \WP_Query( $args ); + /** + * Test the `ep_woocommerce_orders_autosuggest_available` filter + */ + add_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_true' ); + $this->assertTrue( $woocommerce_feature->is_orders_autosuggest_available() ); - $this->assertTrue( $query->elasticsearch_success ); - $this->assertEquals( 1, $query->post_count ); - $this->assertEquals( 1, $query->found_posts ); + add_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_false' ); + $this->assertFalse( $woocommerce_feature->is_orders_autosuggest_available() ); } /** - * Test search integration is on in general for product searches + * Test the `is_orders_autosuggest_available` method * - * @since 2.1 + * @since 4.5.0 * @group woocommerce */ - public function testSearchOnAllFrontEnd() { - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); + public function testIsOrdersAutosuggestEnabled() { + $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); - ElasticPress\Elasticsearch::factory()->refresh_indices(); + $this->assertFalse( $woocommerce_feature->is_orders_autosuggest_enabled() ); - $args = array( - 's' => 'findme', - 'post_type' => 'product', - ); + /** + * Make it available but it won't be enabled + */ + add_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_true' ); + $this->assertFalse( $woocommerce_feature->is_orders_autosuggest_enabled() ); - $query = new \WP_Query( $args ); + /** + * Enable it + */ + $filter = function() { + return [ + 'woocommerce' => [ + 'orders' => '1', + ], + ]; + }; + add_filter( 'pre_site_option_ep_feature_settings', $filter ); + add_filter( 'pre_option_ep_feature_settings', $filter ); + $this->assertTrue( $woocommerce_feature->is_orders_autosuggest_enabled() ); - $this->assertTrue( $query->elasticsearch_success ); + /** + * Make it unavailable. Even activated, it should not be considered enabled if not available anymore. + */ + remove_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_true' ); + $this->assertFalse( $woocommerce_feature->is_orders_autosuggest_enabled() ); } /** @@ -399,834 +354,4 @@ public function testEPWoocommerceAdminProductsListSearchFields() { [ 'post_title', 'post_content' ] ); } - - /** - * Tests the search query for a shop_coupon. - * - * @since 4.4.1 - */ - public function testSearchQueryForCoupon() { - - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - // ensures that the search query doesn't use Elasticsearch. - $query = new \WP_Query( - [ - 'post_type' => 'shop_coupon', - 's' => 'test-coupon', - ] - ); - $this->assertNull( $query->elasticsearch_success ); - - // ensures that the search query doesn't use Elasticsearch when ep_integrate set to false. - $query = new \WP_Query( - [ - 'post_type' => 'shop_coupon', - 's' => 'test-coupon', - 'ep_integrate' => false, - ] - ); - $this->assertNull( $query->elasticsearch_success ); - - // ensures that the search query use Elasticsearch when ep_integrate set to true. - $query = new \WP_Query( - [ - 'post_type' => 'shop_coupon', - 's' => 'test-coupon', - 'ep_integrate' => true, - ] - ); - $this->assertTrue( $query->elasticsearch_success ); - } - - /** - * Tests the search query for a shop_coupon in admin use Elasticsearch when protected content is enabled. - * - * @since 4.4.1 - */ - public function testSearchQueryForCouponWhenProtectedContentIsEnable() { - - set_current_screen( 'dashboard' ); - $this->assertTrue( is_admin() ); - - ElasticPress\Features::factory()->activate_feature( 'protected_content' ); - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $this->ep_factory->post->create( - array( - 'post_content' => 'test-coupon', - 'post_type' => 'shop_coupon', - ) - ); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); - - $query = new \WP_Query( - [ - 'post_type' => 'shop_coupon', - 's' => 'test-coupon', - ] - ); - - $this->assertTrue( $query->elasticsearch_success ); - $this->assertEquals( 1, $query->post_count ); - } - - /** - * Tests the search query for a shop_coupon in admin does not use Elasticsearch when protected content is not enabled. - * - * @since 4.4.1 - */ - public function testSearchQueryForCouponWhenProtectedContentIsNotEnable() { - - set_current_screen( 'dashboard' ); - $this->assertTrue( is_admin() ); - - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $this->ep_factory->post->create( - array( - 'post_content' => 'test-coupon', - 'post_type' => 'shop_coupon', - ) - ); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); - - $query = new \WP_Query( - [ - 'post_type' => 'shop_coupon', - 's' => 'test-coupon', - 'ep_integrate' => true, - ] - ); - - $this->assertNull( $query->elasticsearch_success ); - $this->assertEquals( 1, $query->post_count ); - } - - /** - * Test all the product attributes are synced. - * - * @since 4.5.0 - */ - public function testWoocommerceAttributeTaxonomiesAreSync() { - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $product_id = $this->ep_factory->product->create_variation_product(); - - $post = new \ElasticPress\Indexable\Post\Post(); - $document = $post->prepare_document( $product_id ); - - $this->assertArrayHasKey( 'pa_size', $document['terms'] ); - $this->assertArrayHasKey( 'pa_colour', $document['terms'] ); - $this->assertArrayHasKey( 'pa_number', $document['terms'] ); - } - - /** - * Data provider for the testProductQueryOrder method. - * - * @return array - */ - public function productQueryOrderDataProvider() : array { - return [ - [ - 'total_sales', - [ 'meta_key' => 'total_sales' ], - false, - [ - 0 => [ 'meta.total_sales.double' => [ 'order' => 'desc' ] ], - 1 => [ 'post_date' => [ 'order' => 'desc' ] ], - ], - ], - [ - 'average_rating', - [ 'meta_key' => '_wc_average_rating' ], - false, - [ - 0 => [ 'meta._wc_average_rating.double' => [ 'order' => 'desc' ] ], - 1 => [ 'post_date' => [ 'order' => 'desc' ] ], - ], - ], - [ - 'regular_price', - [ - 'orderby' => 'price', - 'order' => 'DESC', - ], - false, - [ - 0 => [ 'meta._price.double' => [ 'order' => 'desc' ] ], - 1 => [ 'post_date' => [ 'order' => 'desc' ] ], - ], - ], - [ - 'total_sales', - [ - 'orderby' => 'popularity', - 'order' => 'DESC', - ], - false, - [ - 0 => [ 'meta.total_sales.double' => [ 'order' => 'desc' ] ], - 1 => [ 'post_date' => [ 'order' => 'desc' ] ], - ], - ], - [ - 'total_sales', - [], - 'popularity', - [ - 0 => [ 'meta.total_sales.double' => [ 'order' => 'desc' ] ], - 1 => [ 'post_date' => [ 'order' => 'desc' ] ], - ], - ], - [ - 'regular_price', - [], - 'price-desc', - [ - 0 => [ 'meta._price.double' => [ 'order' => 'desc' ] ], - 1 => [ 'post_date' => [ 'order' => 'desc' ] ], - ], - ], - [ - 'average_rating', - [], - 'rating', - [ - 0 => [ 'meta._wc_average_rating.double' => [ 'order' => 'desc' ] ], - 1 => [ 'post_date' => [ 'order' => 'desc' ] ], - ], - ], - [ - 'regular_price', - [], - 'price', - [ - 0 => [ 'meta._price.double' => [ 'order' => 'asc' ] ], - 1 => [ 'post_date' => [ 'order' => 'asc' ] ], - ], - 'asc', - ], - [ - 'sku', - [], - 'sku', - [ - 0 => [ 'meta._sku.value.sortable' => [ 'order' => 'asc' ] ], - 1 => [ 'post_date' => [ 'order' => 'asc' ] ], - ], - 'asc', - ], - [ - 'name', - [], - 'title', - [ - 0 => [ 'post_title.sortable' => [ 'order' => 'asc' ] ], - 1 => [ 'post_date' => [ 'order' => 'asc' ] ], - ], - 'asc', - ], - [ - '', - [], - 'default', - [ - 0 => [ 'menu_order' => [ 'order' => 'asc' ] ], - 1 => [ 'post_title.sortable' => [ 'order' => 'asc' ] ], - 2 => [ 'post_date' => [ 'order' => 'asc' ] ], - ], - ], - [ '', [], '', [ 0 => [ 'post_date' => [ 'order' => 'desc' ] ] ] ], - ]; - } - - /** - * Test the product query order. - * - * @param string $product_arg_key Field slug - * @param array $query_args Query array - * @param bool $query_string Query string - * @param array $expected Value expected - * @param string $order Order - * @dataProvider productQueryOrderDataProvider - * @since 4.5.0 - */ - public function testProductQueryOrder( $product_arg_key, $query_args, $query_string, $expected, $order = '' ) { - global $wp_the_query; - - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $product_1 = $this->ep_factory->product->create( - array( - $product_arg_key => 200, - ) - ); - - $product_2 = $this->ep_factory->product->create( - array( - $product_arg_key => 100, - ) - ); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); - - if ( $query_string ) { - parse_str( 'orderby=' . $query_string, $_GET ); - - // mock the query as post type archive - add_action( - 'parse_query', - function( \WP_Query $query ) { - $query->is_post_type_archive = true; - } - ); - } - - $args = array( - 'post_type' => 'product', - ); - $args = array_merge( $args, $query_args ); - $query = new \WP_Query( $args ); - - // mock the query as main query - $wp_the_query = $query; - - add_filter( - 'ep_post_formatted_args', - function ( $formatted_args ) use ( $expected ) { - $this->assertEquals( $expected, $formatted_args['sort'] ); - return $formatted_args; - } - ); - - $query = $query->query( $args ); - - $this->assertTrue( $wp_the_query->elasticsearch_success, 'Elasticsearch query failed' ); - $this->assertEquals( 2, count( $query ) ); - - if ( 'asc' === $order ) { - $this->assertEquals( $product_2, $query[0]->ID ); - $this->assertEquals( $product_1, $query[1]->ID ); - } elseif ( 'desc' === $order ) { - $this->assertEquals( $product_1, $query[0]->ID ); - $this->assertEquals( $product_2, $query[1]->ID ); - } - - \WC_Query::reset_chosen_attributes(); - } - - /** - * Test the product query not use Elasticsearch if preview. - * - * @since 4.5.0 - */ - public function testQueryShouldNotUseElasticsearchIfPreview() { - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $args = array( - 'post_type' => 'product', - 'preview' => true, - ); - - $query = new \WP_Query( $args ); - - $this->assertNull( $query->elasticsearch_success ); - } - - /** - * Test that on Admin Product List use Elasticsearch. - * - * @since 4.5.0 - */ - public function testProductListInAdminUseElasticSearch() { - global $typenow, $wc_list_table; - - set_current_screen( 'edit.php' ); - $this->assertTrue( is_admin() ); - - // load required files - include_once ABSPATH . 'wp-admin/includes/class-wp-posts-list-table.php'; - include_once WC()->plugin_path() . '/includes/admin/list-tables/class-wc-admin-list-table-products.php'; - - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->activate_feature( 'protected_content' ); - ElasticPress\Features::factory()->setup_features(); - - // mock the global variables - $typenow = 'product'; - $wc_list_table = new \WC_Admin_List_Table_Products(); - - add_filter( - 'ep_post_filters', - function( $filters, $args, $query ) { - $expected_result = array( - 'terms' => array( - 'post_type.raw' => array( - 'product', - ), - ), - ); - - $this->assertEquals( $expected_result, $filters['post_type'] ); - return $filters; - }, - 10, - 3 - ); - - parse_str( 'post_type=product&s=product', $_GET ); - - $wp_list_table = new \WP_Posts_List_Table(); - $wp_list_table->prepare_items(); - } - - /** - * Test that Search in Admin Product List use Elasticsearch. - * - * @since 4.5.0 - */ - public function testProductListSearchInAdminUseElasticSearch() { - global $typenow, $wc_list_table; - - set_current_screen( 'edit.php' ); - $this->assertTrue( is_admin() ); - - // load required files - include_once ABSPATH . 'wp-admin/includes/class-wp-posts-list-table.php'; - include_once WC()->plugin_path() . '/includes/admin/list-tables/class-wc-admin-list-table-products.php'; - - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->activate_feature( 'protected_content' ); - ElasticPress\Features::factory()->setup_features(); - - // mock the global variables - $typenow = 'product'; - $wc_list_table = new \WC_Admin_List_Table_Products(); - - add_filter( - 'ep_post_formatted_args', - function ( $formatted_args, $args, $wp_query ) { - $this->assertEquals( 'findme', $formatted_args['query']['function_score']['query']['bool']['should'][0]['multi_match']['query'] ); - $this->assertEquals( - $args['search_fields'], - [ - 'post_title', - 'post_content', - 'post_excerpt', - 'meta' => [ - '_sku', - '_variations_skus', - ], - ] - ); - - return $formatted_args; - }, - 10, - 3 - ); - - parse_str( 'post_type=product&s=findme', $_GET ); - - $wp_list_table = new \WP_Posts_List_Table(); - $wp_list_table->prepare_items(); - } - - /** - * Test the product query when price filter is set. - * - * @since 4.5.0 - */ - public function testPriceFilter() { - global $wp_the_query, $wp_query; - - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $this->ep_factory->product->create( - [ - 'name' => 'Cap 1', - 'regular_price' => 100, - ] - ); - $this->ep_factory->product->create( - [ - 'name' => 'Cap 2', - 'regular_price' => 800, - ] - ); - $this->ep_factory->product->create( - [ - 'name' => 'Cap 3', - 'regular_price' => 10000, - ] - ); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); - - parse_str( 'min_price=1&max_price=999', $_GET ); - - $args = array( - 'post_type' => 'product', - ); - $query = new \WP_Query( $args ); - - // mock the query as main query and is_search - $wp_the_query = $query; - $wp_query->is_search = true; - - add_filter( - 'ep_post_formatted_args', - function ( $formatted_args ) { - - $expected_result = array( - 'range' => array( - 'meta._price.long' => array( - 'gte' => 1, - 'lte' => 999, - 'boost' => 2, - ), - ), - ); - - $this->assertEquals( $expected_result, $formatted_args['query'] ); - return $formatted_args; - }, - 15 - ); - - $query = $query->query( $args ); - - $this->assertTrue( $wp_the_query->elasticsearch_success, 'Elasticsearch query failed' ); - $this->assertEquals( 2, count( $query ) ); - } - - /** - * Test the product search query when price filter is set. - * - * @since 4.5.0 - */ - public function testPriceFilterWithSearchQuery() { - global $wp_the_query, $wp_query; - - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $this->ep_factory->product->create( - [ - 'name' => 'Cap 1', - 'regular_price' => 100, - ] - ); - - $this->ep_factory->product->create( - [ - 'name' => 'Cap 2', - 'regular_price' => 1000, - ] - ); - - $this->ep_factory->product->create( - [ - 'name' => 'Cap 3', - 'regular_price' => 10000, - ] - ); - - $this->ep_factory->product->create( - [ - 'name' => 'Cap 4', - 'regular_price' => 800, - ] - ); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); - - parse_str( 'min_price=1&max_price=999', $_GET ); - - $args = array( - 's' => 'Cap', - 'post_type' => 'product', - ); - $query = new \WP_Query( $args ); - - // mock the query as main query and is_search - $wp_the_query = $query; - $wp_query->is_search = true; - - $query = $query->query( $args ); - - $this->assertTrue( $wp_the_query->elasticsearch_success, 'Elasticsearch query failed' ); - $this->assertEquals( 2, count( $query ) ); - } - - /** - * Tests that attributes filter uses Elasticsearch. - * - * @since 4.5.0 - */ - public function testAttributesFilterUseES() { - global $wp_the_query; - - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $this->ep_factory->product->create_variation_product( - [ - 'name' => 'Cap', - ] - ); - - $this->ep_factory->product->create( - [ - 'name' => 'Shoes', - ] - ); - - $this->ep_factory->product->create( - [ - 'name' => 'T-Shirt', - ] - ); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); - - // mock the query as post type archive - add_action( - 'parse_query', - function( \WP_Query $query ) { - $query->is_post_type_archive = true; - } - ); - - parse_str( 'filter_colour=blue', $_GET ); - - $args = array( - 'post_type' => 'product', - ); - $query = new \WP_Query( $args ); - - // mock the query as main query - $wp_the_query = $query; - - $query = $query->query( $args ); - - $this->assertTrue( $wp_the_query->elasticsearch_success ); - $this->assertEquals( 1, count( $query ) ); - $this->assertEquals( 'Cap', $query[0]->post_title ); - } - - /** - * Tests that get_posts() uses Elasticsearch when ep_integrate is true. - * - * @since 4.5.0 - */ - public function testGetPosts() { - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $this->ep_factory->product->create(); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); - - $posts = get_posts( - [ - 'post_type' => 'product', - 'ep_integrate' => true, - ] - ); - - $this->assertTrue( $posts[0]->elasticsearch ); - } - - /** - * Tests that get_posts() does not use Elasticsearch when ep_integrate is not set. - * - * @since 4.5.0 - */ - public function testGetPostQueryDoesNotUseElasticSearchByDefault() { - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $this->ep_factory->product->create(); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); - - $posts = get_posts( - [ - 'post_type' => 'product', - ] - ); - - $properties = get_object_vars( $posts[0] ); - $this->assertArrayNotHasKey( 'elasticsearch', $properties ); - } - - /** - * Tests that Weighting dashboard shows SKU and Variation SKUs option. - * - * @since 4.5.0 - */ - public function testSkuOptionAddInWeightDashboard() { - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - $search = ElasticPress\Features::factory()->get_registered_feature( 'search' ); - $fields = $search->weighting->get_weightable_fields_for_post_type( 'product' ); - - $this->assertArrayHasKey( 'meta._sku.value', $fields['attributes']['children'] ); - $this->assertArrayHasKey( 'meta._variations_skus.value', $fields['attributes']['children'] ); - - $this->assertEquals( 'meta._sku.value', $fields['attributes']['children']['meta._sku.value']['key'] ); - $this->assertEquals( 'SKU', $fields['attributes']['children']['meta._sku.value']['label'] ); - - $this->assertEquals( 'meta._variations_skus.value', $fields['attributes']['children']['meta._variations_skus.value']['key'] ); - $this->assertEquals( 'Variations SKUs', $fields['attributes']['children']['meta._variations_skus.value']['label'] ); - } - - /** - * Test the `is_orders_autosuggest_available` method - * - * @since 4.5.0 - */ - public function testIsOrdersAutosuggestAvailable() { - $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); - - $this->assertSame( $woocommerce_feature->is_orders_autosuggest_available(), \ElasticPress\Utils\is_epio() ); - - /** - * Test the `ep_woocommerce_orders_autosuggest_available` filter - */ - add_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_true' ); - $this->assertTrue( $woocommerce_feature->is_orders_autosuggest_available() ); - - add_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_false' ); - $this->assertFalse( $woocommerce_feature->is_orders_autosuggest_available() ); - } - - /** - * Test the `is_orders_autosuggest_available` method - * - * @since 4.5.0 - */ - public function testIsOrdersAutosuggestEnabled() { - $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); - - $this->assertFalse( $woocommerce_feature->is_orders_autosuggest_enabled() ); - - /** - * Make it available but it won't be enabled - */ - add_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_true' ); - $this->assertFalse( $woocommerce_feature->is_orders_autosuggest_enabled() ); - - /** - * Enable it - */ - $filter = function() { - return [ - 'woocommerce' => [ - 'orders' => '1', - ], - ]; - }; - add_filter( 'pre_site_option_ep_feature_settings', $filter ); - add_filter( 'pre_option_ep_feature_settings', $filter ); - $this->assertTrue( $woocommerce_feature->is_orders_autosuggest_enabled() ); - - /** - * Make it unavailable. Even activated, it should not be considered enabled if not available anymore. - */ - remove_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_true' ); - $this->assertFalse( $woocommerce_feature->is_orders_autosuggest_enabled() ); - } - - /** - * Test if decaying is disabled on products. - * - * @since 4.6.0 - * @dataProvider decayingDisabledOnProductsProvider - * @group woocommerce - * - * @param string $setting Value for `decaying_enabled` - * @param array|string $post_type Post types to be queried - * @param string $assert Assert method name (`assertDecayDisabled` or `assertDecayEnabled`) - */ - public function testDecayingDisabledOnProducts( $setting, $post_type, $assert ) { - ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); - ElasticPress\Features::factory()->setup_features(); - - // Test decaying for product query when disabled_only_products is enabled - ElasticPress\Features::factory()->update_feature( - 'search', - [ - 'active' => true, - 'decaying_enabled' => $setting, - ] - ); - - $query = new \WP_Query(); - $query_args = [ - 's' => 'test', - 'post_type' => $post_type, - ]; - $formatted_args = \ElasticPress\Indexables::factory()->get( 'post' )->format_args( $query_args, $query ); - - $this->$assert( $formatted_args['query'] ); - } - - /** - * Data provider for the testDecayingDisabledOnProducts method. - * - * @since 4.6.0 - * @return array - */ - public function decayingDisabledOnProductsProvider() : array { - return [ - [ - 'disabled_only_products', - 'product', - 'assertDecayDisabled', - ], - [ - 'disabled_only_products', - [ 'product' ], - 'assertDecayDisabled', - ], - [ - 'disabled_only_products', - [ 'product', 'post' ], - 'assertDecayEnabled', - ], - [ - 'disabled_includes_products', - 'product', - 'assertDecayDisabled', - ], - [ - 'disabled_includes_products', - [ 'product' ], - 'assertDecayDisabled', - ], - [ - 'disabled_includes_products', - [ 'product', 'post' ], - 'assertDecayDisabled', - ], - [ - 'disabled_includes_products', - [ 'post', 'page' ], - 'assertDecayEnabled', - ], - ]; - } } diff --git a/tests/php/features/WooCommerce/TestWooCommerceOrders.php b/tests/php/features/WooCommerce/TestWooCommerceOrders.php index b581a2a3c..852c631e9 100644 --- a/tests/php/features/WooCommerce/TestWooCommerceOrders.php +++ b/tests/php/features/WooCommerce/TestWooCommerceOrders.php @@ -14,6 +14,165 @@ * WC orders test class */ class TestWooCommerceOrders extends TestWooCommerce { + /** + * Test search integration is on for shop orders + * + * @group woocommerce + * @group woocommerce-orders + */ + public function testSearchOnShopOrderAdmin() { + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $this->ep_factory->post->create( + array( + 'post_content' => 'findme', + 'post_type' => 'shop_order', + ) + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + // mock the pagenow to bypass the search_order checks + global $pagenow; + $pagenow = 'edit.php'; + + parse_str( 's=findme', $_GET ); + $args = array( + 's' => 'findme', + 'post_type' => 'shop_order', + ); + + $query = new \WP_Query( $args ); + + $this->assertTrue( $query->elasticsearch_success ); + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + + $pagenow = 'index.php'; + } + + /** + * Test Shop Order post type query does not get integrated when the protected content feature is deactivated. + * + * @group woocommerce + * @group woocommerce-orders + */ + public function testShopOrderPostTypeQueryOn() { + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $this->ep_factory->post->create(); + $this->ep_factory->post->create( + array( + 'post_type' => 'shop_order', + ) + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $args = array( + 'post_type' => 'shop_order', + ); + $query = new \WP_Query( $args ); + + $this->assertNull( $query->elasticsearch_success ); + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + } + + + /** + * Test Shop Order post type query does get integrated when the protected content feature is activated. + * + * @group woocommerce + * @group woocommerce-orders + */ + public function testShopOrderPostTypeQueryWhenProtectedContentEnable() { + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $this->ep_factory->post->create(); + $this->ep_factory->post->create( + array( + 'post_type' => 'shop_order', + ) + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $args = array( + 'post_type' => 'shop_order', + ); + $query = new \WP_Query( $args ); + + $this->assertTrue( $query->elasticsearch_success ); + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + } + + /** + * Test Shop Order post type query does not get integrated when the protected content feature is activated and ep_integrate is set to false. + * + * @group woocommerce + * @group woocommerce-orders + */ + public function testShopOrderPostTypeQueryWhenEPIntegrateSetFalse() { + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $this->ep_factory->post->create(); + $this->ep_factory->post->create( + array( + 'post_type' => 'shop_order', + ) + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $args = array( + 'post_type' => 'shop_order', + 'ep_integrate' => false, + ); + $query = new \WP_Query( $args ); + + $this->assertNull( $query->elasticsearch_success ); + } + + /** + * Test search for shop orders by order ID + * + * @group woocommerce + * @group woocommerce-orders + */ + public function testSearchShopOrderById() { + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $shop_order_id = $this->ep_factory->post->create( + array( + 'post_type' => 'shop_order', + ) + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $args = array( + 's' => (string) $shop_order_id, + 'post_type' => 'shop_order', + ); + + $query = new \WP_Query( $args ); + + $this->assertTrue( $query->elasticsearch_success ); + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + } + /** * Test search for shop orders matching field and ID. * @@ -21,6 +180,7 @@ class TestWooCommerceOrders extends TestWooCommerce { * both should be returned. * * @group woocommerce + * @group woocommerce-orders */ public function testSearchShopOrderByMetaFieldAndId() { ElasticPress\Features::factory()->activate_feature( 'protected_content' ); diff --git a/tests/php/features/WooCommerce/TestWooCommerceProduct.php b/tests/php/features/WooCommerce/TestWooCommerceProduct.php index 80e375d0d..dc53a9eda 100644 --- a/tests/php/features/WooCommerce/TestWooCommerceProduct.php +++ b/tests/php/features/WooCommerce/TestWooCommerceProduct.php @@ -13,11 +13,697 @@ /** * WC products test class */ -class TestWooCommerceProduct extends BaseTestCase { +class TestWooCommerceProduct extends TestWooCommerce { + /** + * Test products post type query does not get integrated when the feature is active + * + * @group woocommerce + * @group woocommerce-products + */ + public function testProductsPostTypeQueryOn() { + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $this->ep_factory->post->create(); + $this->ep_factory->product->create( + array( + 'description' => 'product 1', + ) + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $args = array( + 'post_type' => 'product', + ); + + $query = new \WP_Query( $args ); + + $this->assertNull( $query->elasticsearch_success ); + $this->assertEquals( 1, $query->post_count ); + $this->assertEquals( 1, $query->found_posts ); + } + + /** + * Test products post type query does get integrated when querying WC product_cat taxonomy + * + * @group woocommerce + * @group woocommerce-products + */ + public function testProductsPostTypeQueryProductCatTax() { + ElasticPress\Features::factory()->activate_feature( 'admin' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $this->ep_factory->post->create(); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $args = array( + 'tax_query' => array( + array( + 'taxonomy' => 'product_cat', + 'terms' => array( 'cat' ), + 'field' => 'slug', + ), + ), + ); + + $query = new \WP_Query( $args ); + + $this->assertTrue( $query->elasticsearch_success ); + + $args = [ 'product_cat' => 'cat' ]; + + $query = new \WP_Query( $args ); + + $this->assertTrue( $query->elasticsearch_success ); + } + + + /** + * Test search integration is on in general for product searches + * + * @group woocommerce + * @group woocommerce-products + */ + public function testSearchOnAllFrontEnd() { + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $args = array( + 's' => 'findme', + 'post_type' => 'product', + ); + + $query = new \WP_Query( $args ); + + $this->assertTrue( $query->elasticsearch_success ); + } + + /** + * Test all the product attributes are synced. + * + * @group woocommerce + * @group woocommerce-products + */ + public function testWoocommerceAttributeTaxonomiesAreSync() { + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $product_id = $this->ep_factory->product->create_variation_product(); + + $post = new \ElasticPress\Indexable\Post\Post(); + $document = $post->prepare_document( $product_id ); + + $this->assertArrayHasKey( 'pa_size', $document['terms'] ); + $this->assertArrayHasKey( 'pa_colour', $document['terms'] ); + $this->assertArrayHasKey( 'pa_number', $document['terms'] ); + } + + /** + * Data provider for the testProductQueryOrder method. + * + * @return array + */ + public function productQueryOrderDataProvider() : array { + return [ + [ + 'total_sales', + [ 'meta_key' => 'total_sales' ], + false, + [ + 0 => [ 'meta.total_sales.double' => [ 'order' => 'desc' ] ], + 1 => [ 'post_date' => [ 'order' => 'desc' ] ], + ], + ], + [ + 'average_rating', + [ 'meta_key' => '_wc_average_rating' ], + false, + [ + 0 => [ 'meta._wc_average_rating.double' => [ 'order' => 'desc' ] ], + 1 => [ 'post_date' => [ 'order' => 'desc' ] ], + ], + ], + [ + 'regular_price', + [ + 'orderby' => 'price', + 'order' => 'DESC', + ], + false, + [ + 0 => [ 'meta._price.double' => [ 'order' => 'desc' ] ], + 1 => [ 'post_date' => [ 'order' => 'desc' ] ], + ], + ], + [ + 'total_sales', + [ + 'orderby' => 'popularity', + 'order' => 'DESC', + ], + false, + [ + 0 => [ 'meta.total_sales.double' => [ 'order' => 'desc' ] ], + 1 => [ 'post_date' => [ 'order' => 'desc' ] ], + ], + ], + [ + 'total_sales', + [], + 'popularity', + [ + 0 => [ 'meta.total_sales.double' => [ 'order' => 'desc' ] ], + 1 => [ 'post_date' => [ 'order' => 'desc' ] ], + ], + ], + [ + 'regular_price', + [], + 'price-desc', + [ + 0 => [ 'meta._price.double' => [ 'order' => 'desc' ] ], + 1 => [ 'post_date' => [ 'order' => 'desc' ] ], + ], + ], + [ + 'average_rating', + [], + 'rating', + [ + 0 => [ 'meta._wc_average_rating.double' => [ 'order' => 'desc' ] ], + 1 => [ 'post_date' => [ 'order' => 'desc' ] ], + ], + ], + [ + 'regular_price', + [], + 'price', + [ + 0 => [ 'meta._price.double' => [ 'order' => 'asc' ] ], + 1 => [ 'post_date' => [ 'order' => 'asc' ] ], + ], + 'asc', + ], + [ + 'sku', + [], + 'sku', + [ + 0 => [ 'meta._sku.value.sortable' => [ 'order' => 'asc' ] ], + 1 => [ 'post_date' => [ 'order' => 'asc' ] ], + ], + 'asc', + ], + [ + 'name', + [], + 'title', + [ + 0 => [ 'post_title.sortable' => [ 'order' => 'asc' ] ], + 1 => [ 'post_date' => [ 'order' => 'asc' ] ], + ], + 'asc', + ], + [ + '', + [], + 'default', + [ + 0 => [ 'menu_order' => [ 'order' => 'asc' ] ], + 1 => [ 'post_title.sortable' => [ 'order' => 'asc' ] ], + 2 => [ 'post_date' => [ 'order' => 'asc' ] ], + ], + ], + [ '', [], '', [ 0 => [ 'post_date' => [ 'order' => 'desc' ] ] ] ], + ]; + } + + /** + * Test the product query order. + * + * @param string $product_arg_key Field slug + * @param array $query_args Query array + * @param bool $query_string Query string + * @param array $expected Value expected + * @param string $order Order + * @dataProvider productQueryOrderDataProvider + * @group woocommerce + * @group woocommerce-products + */ + public function testProductQueryOrder( $product_arg_key, $query_args, $query_string, $expected, $order = '' ) { + global $wp_the_query; + + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $product_1 = $this->ep_factory->product->create( + array( + $product_arg_key => 200, + ) + ); + + $product_2 = $this->ep_factory->product->create( + array( + $product_arg_key => 100, + ) + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + if ( $query_string ) { + parse_str( 'orderby=' . $query_string, $_GET ); + + // mock the query as post type archive + add_action( + 'parse_query', + function( \WP_Query $query ) { + $query->is_post_type_archive = true; + } + ); + } + + $args = array( + 'post_type' => 'product', + ); + $args = array_merge( $args, $query_args ); + $query = new \WP_Query( $args ); + + // mock the query as main query + $wp_the_query = $query; + + add_filter( + 'ep_post_formatted_args', + function ( $formatted_args ) use ( $expected ) { + $this->assertEquals( $expected, $formatted_args['sort'] ); + return $formatted_args; + } + ); + + $query = $query->query( $args ); + + $this->assertTrue( $wp_the_query->elasticsearch_success, 'Elasticsearch query failed' ); + $this->assertEquals( 2, count( $query ) ); + + if ( 'asc' === $order ) { + $this->assertEquals( $product_2, $query[0]->ID ); + $this->assertEquals( $product_1, $query[1]->ID ); + } elseif ( 'desc' === $order ) { + $this->assertEquals( $product_1, $query[0]->ID ); + $this->assertEquals( $product_2, $query[1]->ID ); + } + + \WC_Query::reset_chosen_attributes(); + } + + /** + * Test the product query not use Elasticsearch if preview. + * + * @group woocommerce + * @group woocommerce-products + */ + public function testQueryShouldNotUseElasticsearchIfPreview() { + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $args = array( + 'post_type' => 'product', + 'preview' => true, + ); + + $query = new \WP_Query( $args ); + + $this->assertNull( $query->elasticsearch_success ); + } + + + /** + * Test that on Admin Product List use Elasticsearch. + * + * @group woocommerce + * @group woocommerce-products + */ + public function testProductListInAdminUseElasticSearch() { + global $typenow, $wc_list_table; + + set_current_screen( 'edit.php' ); + $this->assertTrue( is_admin() ); + + // load required files + include_once ABSPATH . 'wp-admin/includes/class-wp-posts-list-table.php'; + include_once WC()->plugin_path() . '/includes/admin/list-tables/class-wc-admin-list-table-products.php'; + + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->setup_features(); + + // mock the global variables + $typenow = 'product'; + $wc_list_table = new \WC_Admin_List_Table_Products(); + + add_filter( + 'ep_post_filters', + function( $filters, $args, $query ) { + $expected_result = array( + 'terms' => array( + 'post_type.raw' => array( + 'product', + ), + ), + ); + + $this->assertEquals( $expected_result, $filters['post_type'] ); + return $filters; + }, + 10, + 3 + ); + + parse_str( 'post_type=product&s=product', $_GET ); + + $wp_list_table = new \WP_Posts_List_Table(); + $wp_list_table->prepare_items(); + } + + /** + * Test that Search in Admin Product List use Elasticsearch. + * + * @group woocommerce + * @group woocommerce-products + */ + public function testProductListSearchInAdminUseElasticSearch() { + global $typenow, $wc_list_table; + + set_current_screen( 'edit.php' ); + $this->assertTrue( is_admin() ); + + // load required files + include_once ABSPATH . 'wp-admin/includes/class-wp-posts-list-table.php'; + include_once WC()->plugin_path() . '/includes/admin/list-tables/class-wc-admin-list-table-products.php'; + + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->setup_features(); + + // mock the global variables + $typenow = 'product'; + $wc_list_table = new \WC_Admin_List_Table_Products(); + + add_filter( + 'ep_post_formatted_args', + function ( $formatted_args, $args, $wp_query ) { + $this->assertEquals( 'findme', $formatted_args['query']['function_score']['query']['bool']['should'][0]['multi_match']['query'] ); + $this->assertEquals( + $args['search_fields'], + [ + 'post_title', + 'post_content', + 'post_excerpt', + 'meta' => [ + '_sku', + '_variations_skus', + ], + ] + ); + + return $formatted_args; + }, + 10, + 3 + ); + + parse_str( 'post_type=product&s=findme', $_GET ); + + $wp_list_table = new \WP_Posts_List_Table(); + $wp_list_table->prepare_items(); + } + + /** + * Test the product query when price filter is set. + * + * @group woocommerce + * @group woocommerce-products + */ + public function testPriceFilter() { + global $wp_the_query, $wp_query; + + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $this->ep_factory->product->create( + [ + 'name' => 'Cap 1', + 'regular_price' => 100, + ] + ); + $this->ep_factory->product->create( + [ + 'name' => 'Cap 2', + 'regular_price' => 800, + ] + ); + $this->ep_factory->product->create( + [ + 'name' => 'Cap 3', + 'regular_price' => 10000, + ] + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + parse_str( 'min_price=1&max_price=999', $_GET ); + + $args = array( + 'post_type' => 'product', + ); + $query = new \WP_Query( $args ); + + // mock the query as main query and is_search + $wp_the_query = $query; + $wp_query->is_search = true; + + add_filter( + 'ep_post_formatted_args', + function ( $formatted_args ) { + + $expected_result = array( + 'range' => array( + 'meta._price.long' => array( + 'gte' => 1, + 'lte' => 999, + 'boost' => 2, + ), + ), + ); + + $this->assertEquals( $expected_result, $formatted_args['query'] ); + return $formatted_args; + }, + 15 + ); + + $query = $query->query( $args ); + + $this->assertTrue( $wp_the_query->elasticsearch_success, 'Elasticsearch query failed' ); + $this->assertEquals( 2, count( $query ) ); + } + + /** + * Test the product search query when price filter is set. + * + * @group woocommerce + * @group woocommerce-products + */ + public function testPriceFilterWithSearchQuery() { + global $wp_the_query, $wp_query; + + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $this->ep_factory->product->create( + [ + 'name' => 'Cap 1', + 'regular_price' => 100, + ] + ); + + $this->ep_factory->product->create( + [ + 'name' => 'Cap 2', + 'regular_price' => 1000, + ] + ); + + $this->ep_factory->product->create( + [ + 'name' => 'Cap 3', + 'regular_price' => 10000, + ] + ); + + $this->ep_factory->product->create( + [ + 'name' => 'Cap 4', + 'regular_price' => 800, + ] + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + parse_str( 'min_price=1&max_price=999', $_GET ); + + $args = array( + 's' => 'Cap', + 'post_type' => 'product', + ); + $query = new \WP_Query( $args ); + + // mock the query as main query and is_search + $wp_the_query = $query; + $wp_query->is_search = true; + + $query = $query->query( $args ); + + $this->assertTrue( $wp_the_query->elasticsearch_success, 'Elasticsearch query failed' ); + $this->assertEquals( 2, count( $query ) ); + } + + /** + * Tests that attributes filter uses Elasticsearch. + * + * @group woocommerce + * @group woocommerce-products + */ + public function testAttributesFilterUseES() { + global $wp_the_query; + + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $this->ep_factory->product->create_variation_product( + [ + 'name' => 'Cap', + ] + ); + + $this->ep_factory->product->create( + [ + 'name' => 'Shoes', + ] + ); + + $this->ep_factory->product->create( + [ + 'name' => 'T-Shirt', + ] + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + // mock the query as post type archive + add_action( + 'parse_query', + function( \WP_Query $query ) { + $query->is_post_type_archive = true; + } + ); + + parse_str( 'filter_colour=blue', $_GET ); + + $args = array( + 'post_type' => 'product', + ); + $query = new \WP_Query( $args ); + + // mock the query as main query + $wp_the_query = $query; + + $query = $query->query( $args ); + + $this->assertTrue( $wp_the_query->elasticsearch_success ); + $this->assertEquals( 1, count( $query ) ); + $this->assertEquals( 'Cap', $query[0]->post_title ); + } + + /** + * Tests that get_posts() uses Elasticsearch when ep_integrate is true. + * + * @group woocommerce + * @group woocommerce-products + */ + public function testGetPosts() { + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $this->ep_factory->product->create(); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $posts = get_posts( + [ + 'post_type' => 'product', + 'ep_integrate' => true, + ] + ); + + $this->assertTrue( $posts[0]->elasticsearch ); + } + + /** + * Tests that get_posts() does not use Elasticsearch when ep_integrate is not set. + * + * @group woocommerce + * @group woocommerce-products + */ + public function testGetPostQueryDoesNotUseElasticSearchByDefault() { + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $this->ep_factory->product->create(); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $posts = get_posts( + [ + 'post_type' => 'product', + ] + ); + + $properties = get_object_vars( $posts[0] ); + $this->assertArrayNotHasKey( 'elasticsearch', $properties ); + } + + /** + * Tests that Weighting dashboard shows SKU and Variation SKUs option. + * + * @group woocommerce + */ + public function testSkuOptionAddInWeightDashboard() { + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $search = ElasticPress\Features::factory()->get_registered_feature( 'search' ); + $fields = $search->weighting->get_weightable_fields_for_post_type( 'product' ); + + $this->assertArrayHasKey( 'meta._sku.value', $fields['attributes']['children'] ); + $this->assertArrayHasKey( 'meta._variations_skus.value', $fields['attributes']['children'] ); + + $this->assertEquals( 'meta._sku.value', $fields['attributes']['children']['meta._sku.value']['key'] ); + $this->assertEquals( 'SKU', $fields['attributes']['children']['meta._sku.value']['label'] ); + + $this->assertEquals( 'meta._variations_skus.value', $fields['attributes']['children']['meta._variations_skus.value']['key'] ); + $this->assertEquals( 'Variations SKUs', $fields['attributes']['children']['meta._variations_skus.value']['label'] ); + } + /** * Test the addition of variations skus to product meta * * @group woocommerce + * @group woocommerce-products */ public function testAddVariationsSkusMeta() { ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); @@ -55,6 +741,7 @@ public function testAddVariationsSkusMeta() { * Test the translate_args_admin_products_list method * * @group woocommerce + * @group woocommerce-products */ public function testTranslateArgsAdminProductsList() { ElasticPress\Features::factory()->activate_feature( 'protected_content' ); @@ -96,6 +783,7 @@ public function testTranslateArgsAdminProductsList() { * Test the ep_woocommerce_admin_products_list_search_fields filter * * @group woocommerce + * @group woocommerce-products */ public function testEPWoocommerceAdminProductsListSearchFields() { ElasticPress\Features::factory()->activate_feature( 'protected_content' ); @@ -122,4 +810,83 @@ public function testEPWoocommerceAdminProductsListSearchFields() { [ 'post_title', 'post_content' ] ); } + + /** + * Test if decaying is disabled on products. + * + * @dataProvider decayingDisabledOnProductsProvider + * @group woocommerce + * @group woocommerce-products + * + * @param string $setting Value for `decaying_enabled` + * @param array|string $post_type Post types to be queried + * @param string $assert Assert method name (`assertDecayDisabled` or `assertDecayEnabled`) + */ + public function testDecayingDisabledOnProducts( $setting, $post_type, $assert ) { + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + // Test decaying for product query when disabled_only_products is enabled + ElasticPress\Features::factory()->update_feature( + 'search', + [ + 'active' => true, + 'decaying_enabled' => $setting, + ] + ); + + $query = new \WP_Query(); + $query_args = [ + 's' => 'test', + 'post_type' => $post_type, + ]; + $formatted_args = \ElasticPress\Indexables::factory()->get( 'post' )->format_args( $query_args, $query ); + + $this->$assert( $formatted_args['query'] ); + } + + /** + * Data provider for the testDecayingDisabledOnProducts method. + * + * @return array + */ + public function decayingDisabledOnProductsProvider() : array { + return [ + [ + 'disabled_only_products', + 'product', + 'assertDecayDisabled', + ], + [ + 'disabled_only_products', + [ 'product' ], + 'assertDecayDisabled', + ], + [ + 'disabled_only_products', + [ 'product', 'post' ], + 'assertDecayEnabled', + ], + [ + 'disabled_includes_products', + 'product', + 'assertDecayDisabled', + ], + [ + 'disabled_includes_products', + [ 'product' ], + 'assertDecayDisabled', + ], + [ + 'disabled_includes_products', + [ 'product', 'post' ], + 'assertDecayDisabled', + ], + [ + 'disabled_includes_products', + [ 'post', 'page' ], + 'assertDecayEnabled', + ], + ]; + } } From b47d9c49f6e2ef3390883c157908da43caba4d11 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Thu, 22 Jun 2023 13:59:04 -0300 Subject: [PATCH 08/11] Test deprecation of moved methods --- .../classes/Feature/WooCommerce/Orders.php | 2 +- .../WooCommerce/TestWooCommerceOrders.php | 48 +++++++++++++++++++ .../WooCommerce/TestWooCommerceProduct.php | 2 +- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/includes/classes/Feature/WooCommerce/Orders.php b/includes/classes/Feature/WooCommerce/Orders.php index 499546c48..6a2ffafa0 100644 --- a/includes/classes/Feature/WooCommerce/Orders.php +++ b/includes/classes/Feature/WooCommerce/Orders.php @@ -433,7 +433,7 @@ public function __call( $method_name, $arguments ) { if ( in_array( $method_name, $orders_autosuggest_methods, true ) ) { _deprecated_function( - __METHOD__, + "\ElasticPress\Feature\WooCommerce\WooCommerce\Orders::{$method_name}", // phpcs:ignore '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders_autosuggest->{$method_name}()" // phpcs:ignore ); diff --git a/tests/php/features/WooCommerce/TestWooCommerceOrders.php b/tests/php/features/WooCommerce/TestWooCommerceOrders.php index 852c631e9..73860ac3d 100644 --- a/tests/php/features/WooCommerce/TestWooCommerceOrders.php +++ b/tests/php/features/WooCommerce/TestWooCommerceOrders.php @@ -213,4 +213,52 @@ public function testSearchShopOrderByMetaFieldAndId() { $this->assertEquals( 2, $query->post_count ); $this->assertEquals( 2, $query->found_posts ); } + + /** + * Test if methods moved to OrdersAutosuggest are correctly flagged + * + * @param string $method The method name + * @param array $args Method arguments + * @dataProvider ordersAutosuggestMethodsDataProvider + * @group woocommerce + * @group woocommerce-orders + */ + public function testOrdersAutosuggestMethods( $method, $args ) { + $this->setExpectedDeprecated( "\ElasticPress\Feature\WooCommerce\WooCommerce\Orders::{$method}" ); + $orders = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders; + $orders->$method( ...$args ); + } + + /** + * Data provider for the testOrdersAutosuggestMethods method. + * + * @return array + */ + public function ordersAutosuggestMethodsDataProvider() : array { + return [ + [ 'after_update_feature', [ 'test', [], [] ] ], + [ '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', [ (object) [] ] ], + [ 'is_integrated_request', [ true, [] ] ], + [ 'post_statuses', [ [] ] ], + [ 'post_types', [ [] ] ], + [ 'mapping', [ [] ] ], + [ 'maybe_query_password_protected_posts', [ [] ] ], + [ 'maybe_set_posts_where', [ '', new \WP_Query( [] ) ] ], + [ 'refresh_token', [] ], + [ 'rest_api_init', [] ], + [ 'set_search_fields', [] ], + ]; + } } diff --git a/tests/php/features/WooCommerce/TestWooCommerceProduct.php b/tests/php/features/WooCommerce/TestWooCommerceProduct.php index dc53a9eda..4cf1d6671 100644 --- a/tests/php/features/WooCommerce/TestWooCommerceProduct.php +++ b/tests/php/features/WooCommerce/TestWooCommerceProduct.php @@ -244,7 +244,7 @@ public function productQueryOrderDataProvider() : array { } /** - * Test the product query order. + * Test the product query order. * * @param string $product_arg_key Field slug * @param array $query_args Query array From 95cb4592fbacfa6ac373f0721e884cc1cf851dfd Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Thu, 22 Jun 2023 14:44:38 -0300 Subject: [PATCH 09/11] Unit tests for filters --- .../classes/Feature/WooCommerce/Orders.php | 21 ++- .../classes/Feature/WooCommerce/Products.php | 24 +++- .../WooCommerce/TestWooCommerceOrders.php | 74 +++++++++- .../TestWooCommerceOrdersAutosuggest.php | 18 ++- .../WooCommerce/TestWooCommerceProduct.php | 132 ++++++++++++++++++ 5 files changed, 254 insertions(+), 15 deletions(-) diff --git a/includes/classes/Feature/WooCommerce/Orders.php b/includes/classes/Feature/WooCommerce/Orders.php index 6a2ffafa0..8dd4da792 100644 --- a/includes/classes/Feature/WooCommerce/Orders.php +++ b/includes/classes/Feature/WooCommerce/Orders.php @@ -279,14 +279,29 @@ public function get_supported_post_types() : array { $post_types = [ 'shop_order', 'shop_order_refund' ]; /** - * Expands or contracts the post_types eligible for indexing. + * 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( 'ep_woocommerce_default_supported_post_types', $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} $post_types Post types + * @return {array} New post types + */ + $supported_post_types = apply_filters( 'ep_woocommerce_orders_supported_post_types', $post_types ); $supported_post_types = array_intersect( $supported_post_types, @@ -302,7 +317,7 @@ public function get_supported_post_types() : array { * @param \WP_Query $query The WP_Query * @return \WP_Query */ - public function maybe_set_search_fields( \WP_Query $query ) { + protected function maybe_set_search_fields( \WP_Query $query ) { $search_term = $this->woocommerce->get_search_term( $query ); if ( empty( $search_term ) ) { return $query; diff --git a/includes/classes/Feature/WooCommerce/Products.php b/includes/classes/Feature/WooCommerce/Products.php index d8a480de6..ce38098bf 100644 --- a/includes/classes/Feature/WooCommerce/Products.php +++ b/includes/classes/Feature/WooCommerce/Products.php @@ -710,14 +710,30 @@ public function get_supported_post_types( \WP_Query $query ) : array { } /** - * Expands or contracts the post_types eligible for indexing. + * 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 + * @param {array} $post_types Post types + * @return {array} New post types */ - $supported_post_types = apply_filters( 'ep_woocommerce_default_supported_post_types', $post_types ); + $supported_post_types = apply_filters_deprecated( + 'ep_woocommerce_default_supported_post_types', + [ $post_types ], + '4.7.0', + 'ep_woocommerce_products_supported_post_types' + ); + + /** + * Expands or contracts the post_types related to products eligible for indexing. + * + * @hook ep_woocommerce_products_supported_post_types + * @since 4.7.0 + * @param {array} $post_types Post types + * @param {WP_Query} $query The WP_Query object + * @return {array} New post types + */ + $supported_post_types = apply_filters( 'ep_woocommerce_products_supported_post_types', $post_types, $query ); $supported_post_types = array_intersect( $supported_post_types, diff --git a/tests/php/features/WooCommerce/TestWooCommerceOrders.php b/tests/php/features/WooCommerce/TestWooCommerceOrders.php index 73860ac3d..9c1eff486 100644 --- a/tests/php/features/WooCommerce/TestWooCommerceOrders.php +++ b/tests/php/features/WooCommerce/TestWooCommerceOrders.php @@ -14,6 +14,24 @@ * WC orders test class */ class TestWooCommerceOrders extends TestWooCommerce { + /** + * Orders instance + * + * @var Orders + */ + protected $orders; + + /** + * Setup each test. + * + * @group woocommerce + * @group woocommerce-orders + */ + public function set_up() { + parent::set_up(); + $this->orders = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders; + } + /** * Test search integration is on for shop orders * @@ -214,6 +232,59 @@ public function testSearchShopOrderByMetaFieldAndId() { $this->assertEquals( 2, $query->found_posts ); } + /** + * Test the `get_admin_searchable_post_types` method + * + * @group woocommerce + * @group woocommerce-orders + */ + public function testGetAdminSearchablePostTypes() { + $default_post_types = $this->orders->get_admin_searchable_post_types(); + $this->assertSame( $default_post_types, [ 'shop_order' ] ); + + /** + * Test the `ep_woocommerce_admin_searchable_post_types` filter + */ + $add_post_type = function ( $post_types ) { + $post_types[] = 'shop_order_custom'; + return $post_types; + }; + add_filter( 'ep_woocommerce_admin_searchable_post_types', $add_post_type ); + + $new_post_types = $this->orders->get_admin_searchable_post_types(); + $this->assertSame( $new_post_types, [ 'shop_order', 'shop_order_custom' ] ); + } + + /** + * Test the `get_supported_post_types` method + * + * @group woocommerce + * @group woocommerce-orders + */ + public function testGetSupportedPostTypes() { + $default_supported = $this->orders->get_supported_post_types(); + $this->assertSame( $default_supported, [] ); + + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $default_supported = $this->orders->get_supported_post_types(); + $this->assertSame( $default_supported, [ 'shop_order', 'shop_order_refund' ] ); + + /** + * Test the `ep_woocommerce_orders_supported_post_types` filter + */ + $add_post_type = function( $post_types ) { + $post_types[] = 'shop_order_custom'; + return $post_types; + }; + add_filter( 'ep_woocommerce_orders_supported_post_types', $add_post_type ); + + $custom_supported = $this->orders->get_supported_post_types(); + $this->assertSame( $custom_supported, [ 'shop_order', 'shop_order_refund' ] ); + } + /** * Test if methods moved to OrdersAutosuggest are correctly flagged * @@ -225,8 +296,7 @@ public function testSearchShopOrderByMetaFieldAndId() { */ public function testOrdersAutosuggestMethods( $method, $args ) { $this->setExpectedDeprecated( "\ElasticPress\Feature\WooCommerce\WooCommerce\Orders::{$method}" ); - $orders = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders; - $orders->$method( ...$args ); + $this->orders->$method( ...$args ); } /** diff --git a/tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php b/tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php index 1fc969c71..de3682e8d 100644 --- a/tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php +++ b/tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php @@ -31,7 +31,8 @@ class TestWooCommerceOrdersAutosuggest extends BaseTestCase { /** * Setup each test. * - * @group WooCommerceOrdersAutosuggest + * @group woocommerce + * @group woocommerce-orders-autosuggest */ public function set_up() { parent::set_up(); @@ -51,7 +52,8 @@ public function set_up() { /** * Test the `filter_term_suggest` method * - * @group WooCommerceOrdersAutosuggest + * @group woocommerce + * @group woocommerce-orders-autosuggest */ public function testFilterTermSuggest() { $order = [ @@ -104,7 +106,8 @@ public function testFilterTermSuggest() { * * This method steps into WooCommerce functionality a bit. * - * @group WooCommerceOrdersAutosuggest + * @group woocommerce + * @group woocommerce-orders-autosuggest */ public function testFilterTermSuggestWithCustomOrderId() { $shop_order_1 = new \WC_Order(); @@ -147,7 +150,8 @@ public function testFilterTermSuggestWithCustomOrderId() { /** * Test the `mapping` method with the ES 7 mapping * - * @group WooCommerceOrdersAutosuggest + * @group woocommerce + * @group woocommerce-orders-autosuggest */ public function testMappingEs7() { $original_mapping = [ @@ -192,7 +196,8 @@ public function testMappingEs7() { /** * Test the `mapping` method with the ES 5 mapping * - * @group WooCommerceOrdersAutosuggest + * @group woocommerce + * @group woocommerce-orders-autosuggest */ public function testMappingEs5() { $change_es_version = function() { @@ -247,7 +252,8 @@ public function testMappingEs5() { /** * Test the `set_search_fields` method * - * @group WooCommerceOrdersAutosuggest + * @group woocommerce + * @group woocommerce-orders-autosuggest */ public function testSetSearchFields() { $original_search_fields = [ 'old_search_field' ]; diff --git a/tests/php/features/WooCommerce/TestWooCommerceProduct.php b/tests/php/features/WooCommerce/TestWooCommerceProduct.php index 4cf1d6671..bb1fda869 100644 --- a/tests/php/features/WooCommerce/TestWooCommerceProduct.php +++ b/tests/php/features/WooCommerce/TestWooCommerceProduct.php @@ -14,6 +14,24 @@ * WC products test class */ class TestWooCommerceProduct extends TestWooCommerce { + /** + * Products instance + * + * @var Products + */ + protected $products; + + /** + * Setup each test. + * + * @group woocommerce + * @group woocommerce-orders + */ + public function set_up() { + parent::set_up(); + $this->products = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products; + } + /** * Test products post type query does not get integrated when the feature is active * @@ -889,4 +907,118 @@ public function decayingDisabledOnProductsProvider() : array { ], ]; } + + /** + * Test the `get_supported_post_types` method + * + * @group woocommerce + * @group woocommerce-products + */ + public function testGetSupportedPostTypes() { + $query = new \WP_Query( [] ); + + $default_supported = $this->products->get_supported_post_types( $query ); + $this->assertSame( $default_supported, [] ); + + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + $default_supported = $this->products->get_supported_post_types( $query ); + $this->assertSame( $default_supported, [ 'product_variation' ] ); + + /** + * Test the `ep_woocommerce_products_supported_post_types` filter + */ + $add_post_type = function( $post_types, $filter_query ) use ( $query ) { + $this->assertSame( $filter_query, $query ); + $post_types[] = 'post'; + return $post_types; + }; + add_filter( 'ep_woocommerce_products_supported_post_types', $add_post_type, 10, 2 ); + + $custom_supported = $this->products->get_supported_post_types( $query ); + $this->assertSame( $custom_supported, [ 'product_variation', 'post' ] ); + + $this->markTestIncomplete( 'This test should also test the addition of the `product` post type under some circumstances.' ); + } + + /** + * Test the `get_supported_taxonomies` method + * + * @group woocommerce + * @group woocommerce-products + */ + public function testGetSupportedTaxonomies() { + $default_supported = $this->products->get_supported_taxonomies(); + $expected = [ + 'product_cat', + 'product_tag', + 'product_type', + 'product_visibility', + 'product_shipping_class', + ]; + $this->assertSame( $default_supported, $expected ); + + /** + * Test the `ep_woocommerce_products_supported_taxonomies` filter + */ + $add_taxonomy = function( $taxonomies ) { + $taxonomies[] = 'custom_category'; + return $taxonomies; + }; + add_filter( 'ep_woocommerce_products_supported_taxonomies', $add_taxonomy ); + + $custom_supported = $this->products->get_supported_taxonomies(); + $this->assertSame( $custom_supported, array_merge( $expected, [ 'custom_category' ] ) ); + } + + /** + * Test the `get_orderby_meta_mapping` method + * + * @dataProvider orderbyMetaMappingDataProvider + * @group woocommerce + * @group woocommerce-products + * + * @param string $meta_key Original meta key value + * @param string $translated Expected translated version + */ + public function testOrderbyMetaMapping( $meta_key, $translated ) { + $this->assertSame( $this->products->get_orderby_meta_mapping( $meta_key ), $translated ); + } + + /** + * Data provider for the testOrderbyMetaMapping method. + * + * @return array + */ + public function orderbyMetaMappingDataProvider() { + return [ + [ 'ID', 'ID' ], + [ 'title', 'title date' ], + [ 'menu_order', 'menu_order title date' ], + [ 'menu_order title', 'menu_order title date' ], + [ 'total_sales', 'meta.total_sales.double date' ], + [ '_wc_average_rating', 'meta._wc_average_rating.double date' ], + [ '_price', 'meta._price.double date' ], + [ '_sku', 'meta._sku.value.sortable date' ], + [ 'custom_parameter', 'date' ], + ]; + } + + /** + * Test the `orderby_meta_mapping` filter + * + * @group woocommerce + * @group woocommerce-products + */ + public function testOrderbyMetaMappingFilter() { + $add_value = function ( $mapping ) { + $mapping['custom_parameter'] = 'meta.custom_parameter.long'; + return $mapping; + }; + add_filter( 'orderby_meta_mapping', $add_value ); + + $this->assertSame( $this->products->get_orderby_meta_mapping( 'custom_parameter' ), 'meta.custom_parameter.long' ); + } } From e4160a45e75124b3db78af9b030c229d29be5e56 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Fri, 23 Jun 2023 14:18:36 -0300 Subject: [PATCH 10/11] Refactor + comments for maybe_disable_decaying --- includes/classes/Feature/WooCommerce/Products.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/includes/classes/Feature/WooCommerce/Products.php b/includes/classes/Feature/WooCommerce/Products.php index ce38098bf..6bcc7e542 100644 --- a/includes/classes/Feature/WooCommerce/Products.php +++ b/includes/classes/Feature/WooCommerce/Products.php @@ -564,24 +564,23 @@ public function add_weight_settings_search( $settings ) { * @return bool */ public function maybe_disable_decaying( $is_decaying_enabled, $settings, $args ) { + // If the decay setting isn't a WooCommerce related option, return if ( ! in_array( $settings['decaying_enabled'], [ 'disabled_only_products', 'disabled_includes_products' ], true ) ) { return $is_decaying_enabled; } + // If the query is not dealing with products, return if ( ! isset( $args['post_type'] ) || ! in_array( 'product', (array) $args['post_type'], true ) ) { return $is_decaying_enabled; } $post_types = (array) $args['post_type']; + // If set to disable decay on product-only queries and have more than one post type, return if ( 'disabled_only_products' === $settings['decaying_enabled'] && count( $post_types ) > 1 ) { return $is_decaying_enabled; } - if ( 'disabled_includes_products' === $settings['decaying_enabled'] && ! in_array( 'product', $post_types, true ) ) { - return $is_decaying_enabled; - } - return false; } From 683454238784fca827fbc47edbacc77c27226b18 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Fri, 7 Jul 2023 11:24:27 -0300 Subject: [PATCH 11/11] Translate WC's "price" and "popularity" in any WP_Query --- .../classes/Feature/WooCommerce/Products.php | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/includes/classes/Feature/WooCommerce/Products.php b/includes/classes/Feature/WooCommerce/Products.php index 6bcc7e542..c415ed5fd 100644 --- a/includes/classes/Feature/WooCommerce/Products.php +++ b/includes/classes/Feature/WooCommerce/Products.php @@ -903,17 +903,13 @@ protected function maybe_set_orderby( \WP_Query $query ) { /** * Set orderby and order for price/popularity when GET param not set */ - if ( isset( $query->query_vars['orderby'], $query->query_vars['order'] ) && $query->is_main_query() ) { - switch ( $query->query_vars['orderby'] ) { - case 'price': - $query->set( 'order', $query->query_vars['order'] ); - $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) ); - break; - case 'popularity': - $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) ); - $query->set( 'order', 'DESC' ); - break; - } + $orderby = $query->get( 'orderby', null ); + if ( $orderby && in_array( $orderby, [ 'price', 'popularity' ], true ) ) { + $order = $query->get( 'order', 'DESC' ); + $query->set( 'order', $order ); + + $orderby_field = 'price' === $orderby ? '_price' : 'total_sales'; + $query->set( 'orderby', $this->get_orderby_meta_mapping( $orderby_field ) ); } /**