diff --git a/includes/classes/Feature/WooCommerce/Orders.php b/includes/classes/Feature/WooCommerce/Orders.php index 9fb7ba10ff..8dd4da7928 100644 --- a/includes/classes/Feature/WooCommerce/Orders.php +++ b/includes/classes/Feature/WooCommerce/Orders.php @@ -1,612 +1,461 @@ index = Indexables::factory()->get( 'post' )->get_index_name(); + public function __construct( WooCommerce $woocommerce ) { + $this->woocommerce = $woocommerce; } /** - * Setup feature functionality. - * - * @return void + * Setup order related hooks */ 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 ); + 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 ); + add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 ); } /** - * Get the endpoint for WooCommerce Orders search. + * Allow order creations on the front end to get synced * - * @return string WooCommerce orders search endpoint. + * @param bool $override Original order perms check value + * @param int $post_id Post ID + * @return bool */ - 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 bypass_order_permissions_check( $override, $post_id ) { + $searchable_post_types = $this->get_admin_searchable_post_types(); - /** - * 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 ); + if ( in_array( get_post_type( $post_id ), $searchable_post_types, true ) ) { + return true; + } + + return $override; } /** - * Get the endpoint for temporary tokens. + * Returns the WooCommerce-oriented post types in admin that EP will search * - * @return string Temporary token endpoint. + * @return array */ - public function get_token_endpoint() { + public function get_admin_searchable_post_types() { + $searchable_post_types = array( 'shop_order' ); + /** - * Filters the temporary token API endpoint. + * Filter admin searchable WooCommerce post types * - * @since 4.5.0 - * @hook ep_token_endpoint - * @param {string} $endpoint Endpoint path. - * @returns {string} Token API endpoint. + * @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_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', - ], - ] - ); + return apply_filters( 'ep_woocommerce_admin_searchable_post_types', $searchable_post_types ); } /** - * Enqueue admin assets. + * Index WooCommerce orders meta fields * - * @param string $hook_suffix The current admin page. + * @param array $meta Existing post meta + * @return array */ - 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 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', + ) ) ); } /** - * 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 + * Add order items as a searchable string. * - * @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. + * 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". * - * @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. + * @param array $post_args Post arguments + * @param string|int $post_id Post id * - * @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 add_order_items_search( $post_args, $post_id ) { + $searchable_post_types = $this->get_admin_searchable_post_types(); - /** - * 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 ); - } + // Make sure it is only WooCommerce orders we touch. + if ( ! in_array( $post_args['post_type'], $searchable_post_types, true ) ) { + return $post_args; + } - /** - * 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 ); + $post_indexable = Indexables::factory()->get( 'post' ); - if ( is_wp_error( $request ) ) { - return $request; + // 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']; + } } - $response = wp_remote_retrieve_body( $request ); + // 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 $response; + return $post_args; } /** - * Generate a search template. + * Prevent order fields from being removed. * - * 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. + * 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. * - * 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. + * @see https://github.com/10up/ElasticPress/issues/2726 * - * @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. + * @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 is_integrated_request( $is_integrated, $context ) { - $supported_contexts = [ - 'search', - 'woocommerce', - ]; + public function keep_order_fields( $skip, $post_args ) { + $searchable_post_types = $this->get_admin_searchable_post_types(); - 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']; + if ( in_array( $post_args['post_type'], $searchable_post_types, true ) ) { + return true; + } - return wp_remote_request( $query['url'], $args ); + return $skip; } /** - * Get schema for search args. + * Sets woocommerce meta search fields to an empty array if we are integrating the main query with ElasticSearch * - * @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. + * 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 * - * @return string|false Authorization header, or false on failure. + * @param \WP_Query $query Current query */ - public function get_token() { - $user_id = get_current_user_id(); + public function maybe_hook_woocommerce_search_fields( $query ) { + global $pagenow, $wp, $wc_list_table; - $credentials = get_user_meta( $user_id, 'ep_token', true ); + if ( ! $this->woocommerce->should_integrate_with_query( $query ) ) { + return; + } - if ( $credentials ) { - return $credentials; + /** + * 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; } - return $this->refresh_token(); + remove_action( 'parse_query', [ $wc_list_table, 'search_custom_fields' ] ); } /** - * Refresh the temporary token. + * 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 * - * @return string|false Authorization header, or false on failure. + * @param WP_Query $wp WP Query */ - public function refresh_token() { - $user_id = get_current_user_id(); + public function search_order( $wp ) { + global $pagenow; - $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; + if ( ! $this->woocommerce->should_integrate_with_query( $wp ) ) { + return; } - $response = wp_remote_retrieve_body( $response ); - $response = json_decode( $response ); + $searchable_post_types = $this->get_admin_searchable_post_types(); - $credentials = base64_encode( "$response->username:$response->clear_password" ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode - - update_user_meta( $user_id, 'ep_token', $credentials ); + 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; + } - return $credentials; + // 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 } /** - * Checks if the token API can be used. + * Determines whether or not ES should be integrating with the provided query * - * @return boolean Whether the token API can be used. + * @param \WP_Query $query Query we might integrate with + * @return bool */ - public function check_token_permission() { + public function should_integrate_with_query( \WP_Query $query ) : bool { /** - * Filters the capability required to use the token API. - * - * @since 4.5.0 - * @hook ep_token_capability - * @param {string} $capability Required capability. + * Check the post type */ - $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(); + $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 array_unique( array_merge( $post_statuses, array_keys( $order_statuses ) ) ); + return false; } /** - * Add term suggestions to be indexed + * Get the supported post types for Order related queries * - * @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; - } + public function get_supported_post_types() : array { + $post_types = [ 'shop_order', 'shop_order_refund' ]; /** - * Add the order number as a meta (text) field, so we can freely search on it. + * 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 */ - $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', - ]; + $supported_post_types = apply_filters_deprecated( + 'ep_woocommerce_default_supported_post_types', + [ $post_types ], + '4.7.0', + 'ep_woocommerce_orders_supported_post_types' + ); - 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']; - } - } + /** + * 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 ); - if ( ! empty( $suggest ) ) { - $post_args['term_suggest'] = $suggest; - } + $supported_post_types = array_intersect( + $supported_post_types, + Indexables::factory()->get( 'post' )->get_indexable_post_types() + ); - return $post_args; + return $supported_post_types; } /** - * Add mapping for suggest fields + * If the query has a search term, add the order fields that need to be searched. * - * @param array $mapping ES mapping. - * @return array + * @param \WP_Query $query The WP_Query + * @return \WP_Query */ - public function mapping( $mapping ) { - $post_indexable = Indexables::factory()->get( 'post' ); + protected function maybe_set_search_fields( \WP_Query $query ) { + $search_term = $this->woocommerce->get_search_term( $query ); + if ( empty( $search_term ) ) { + return $query; + } - $mapping = $post_indexable->add_ngram_analyzer( $mapping ); - $mapping = $post_indexable->add_term_suggest_field( $mapping ); + $searchable_post_types = $this->get_admin_searchable_post_types(); - return $mapping; - } + $post_type = $query->get( 'post_type', false ); + if ( ! in_array( $post_type, $searchable_post_types, true ) ) { + return $query; + } - /** - * 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' => [ + $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', + ) + ) + ); - return $search_fields; + $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 ) + ); } /** - * Allow password protected to be indexed. + * Translate args to ElasticPress compat format. This is the meat of what the feature does * - * 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 + * @param \WP_Query $query WP 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 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 ); + /** - * 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. + * Make sure filters are suppressed */ - $args['ep_orders_has_password'] = true; - $args['has_password'] = null; + $query->query['suppress_filters'] = false; + $query->set( 'suppress_filters', false ); - return $args; + $this->maybe_set_search_fields( $query ); } /** - * Restrict password protected posts back but allow orders. + * Handle calls to OrdersAutosuggest methods * - * @see maybe_query_password_protected_posts - * @param string $where Current where clause - * @param WP_Query $query WP_Query - * @return string + * @param string $method_name The method name + * @param array $arguments Array of arguments */ - public function maybe_set_posts_where( $where, $query ) { - global $wpdb; - - if ( ! $query->get( 'ep_orders_has_password' ) ) { - return $where; - } + 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', + ]; - $where .= " AND ( {$wpdb->posts}.post_password = '' OR {$wpdb->posts}.post_type = 'shop_order' )"; + if ( in_array( $method_name, $orders_autosuggest_methods, true ) ) { + _deprecated_function( + "\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 + ); - return $where; + 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 new file mode 100644 index 0000000000..d2543e9362 --- /dev/null +++ b/includes/classes/Feature/WooCommerce/OrdersAutosuggest.php @@ -0,0 +1,612 @@ +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 0000000000..c415ed5fd6 --- /dev/null +++ b/includes/classes/Feature/WooCommerce/Products.php @@ -0,0 +1,986 @@ +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 ); + + 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 ); + + // 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; + } + + 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'; + } + + /** + * DEPRECATED. Expands or contracts the post_types eligible for indexing. + * + * @hook ep_woocommerce_default_supported_post_types + * @since 4.4.0 + * @param {array} $post_types Post types + * @return {array} New post types + */ + $supported_post_types = apply_filters_deprecated( + 'ep_woocommerce_default_supported_post_types', + [ $post_types ], + '4.7.0', + 'ep_woocommerce_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, + 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 + */ + $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 ) ); + } + + /** + * 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 5bbfe41df8..25cb8868ea 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,566 +70,339 @@ 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 + * Setup all feature filters * - * @param array $meta Existing post meta. - * @param array $post Post arguments array. - * @since 2.1 - * @return array + * @since 2.1 */ - 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', - ) - ) - ); + public function setup() { + if ( ! function_exists( 'WC' ) ) { + return; + } + + $this->products->setup(); + $this->orders->setup(); + + add_filter( 'ep_integrate_search_queries', [ $this, 'disallow_coupons' ], 10, 2 ); + + // 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 ); + + // Orders Autosuggest feature. + if ( $this->is_orders_autosuggest_enabled() ) { + $this->orders_autosuggest->setup(); + } } /** - * 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 + * Given a WP_Query object, return its search term (if any) * - * @param array $posts Post object array. - * @since 2.1 - * @return array + * This method also accounts for the `'search'` parameter used by + * WooCommerce, in addition to the regular `'s'` parameter. + * + * @param \WP_Query $query The WP_Query object + * @return string */ - 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; + public function get_search_term( \WP_Query $query ) : string { + $search = $query->get( 'search' ); + return ( ! empty( $search ) ) ? $search : $query->get( 's', '' ); } /** - * Index Woocommerce taxonomies + * Make search coupons don't go through ES * - * @param array $taxonomies Index taxonomies array. - * @param array $post Post properties array. - * @since 2.1 - * @return array + * @param bool $enabled Coupons enabled or not + * @param WP_Query $query WP Query + * @since 4.7.0 + * @return bool */ - 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 disallow_coupons( $enabled, $query ) { + if ( is_admin() ) { + return $enabled; } - /** - * 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 ); - } - } - } + if ( 'shop_coupon' === $query->get( 'post_type' ) && empty( $query->query_vars['ep_integrate'] ) ) { + return false; } - return array_merge( $taxonomies, $woo_taxonomies ); + return $enabled; } /** - * Disallow duplicated ES queries on Orders page. - * - * @since 2.4 - * - * @param array $value Original filter values. - * @param WP_Query $query WP_Query + * Output feature box long * - * @return array + * @since 2.1 */ - 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; + public function output_feature_box_long() { + ?> +

+ 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', - ); + public function output_feature_box_settings() { + $available = $this->is_orders_autosuggest_available(); + $enabled = $this->is_orders_autosuggest_enabled(); + ?> +
+
+
+
+ +

+ tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ; */ + __( 'You are directly connected to %1$sElasticPress.io%2$s! Enable Orders Autosuggest to enhance Dashboard results and quickly find WooCommerce Orders. %3$sLearn More%4$s.', 'elasticpress' ) : + /* translators: 1: tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ; */ + __( 'Due to the sensitive nature of orders, this autosuggest feature is available only to %1$sElasticPress.io%2$s customers. %3$sLearn More%4$s.', 'elasticpress' ); - $supported_taxonomies = array_merge( $supported_taxonomies, $attribute_taxonomies ); + printf( + wp_kses( $message, 'ep-html' ), + '', + '', + '', + '' + ); + ?> +

+
+
+ get( $taxonomy, false ); - - if ( ! empty( $term ) ) { - $integrate = true; - - $tax_query[] = array( - 'taxonomy' => $taxonomy, - 'field' => 'slug', - 'terms' => (array) $term, - ); - } + if ( ! class_exists( 'WooCommerce' ) ) { + $status->code = 2; + $status->message = esc_html__( 'WooCommerce not installed.', 'elasticpress' ); } - /** - * 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', - ); + return $status; + } - $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'; + /** + * 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( $query ) { + // Lets make sure this doesn't interfere with the CLI + if ( defined( 'WP_CLI' ) && WP_CLI ) { + return false; } - /** - * 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() - ); + if ( defined( 'WC_API_REQUEST' ) && WC_API_REQUEST ) { + return false; + } - // 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 ( isset( $query->query_vars['ep_integrate'] ) && ! filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN ) ) { + return false; } /** - * If we have a WooCommerce specific query, lets hook it to ElasticPress and make the query ElasticSearch friendly + * Filter to skip WP Query integration + * + * @hook ep_skip_query_integration + * @param {bool} $skip True to skip + * @param {WP_Query} $query WP Query to evaluate + * @return {bool} New skip value */ - 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' ); + if ( apply_filters( 'ep_skip_query_integration', false, $query ) ) { + return false; } - // 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' ); + if ( ! Utils\is_integrated_request( $this->slug ) ) { + return false; } /** - * 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 + * Do nothing for single product queries */ - $fields = $query->get( 'fields', false ); - if ( ! version_compare( get_bloginfo( 'version' ), '4.6', '>=' ) && ( 'ids' === $fields || 'id=>parent' === $fields ) ) { - $query->set( 'fields', 'default' ); + $product_name = $query->get( 'product', false ); + if ( ! empty( $product_name ) || $query->is_single() ) { + return false; } /** - * Handle meta queries + * ElasticPress does not yet support post_parent 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 ); + $post_parent = $query->get( 'post_parent', false ); + if ( ! empty( $post_parent ) ) { + return false; } /** - * Make sure filters are suppressed + * If this is just a preview, let's not use Elasticsearch. */ - $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' ); + if ( $query->get( 'preview', false ) ) { + return false; } - $query->query_vars['ep_integrate'] = true; - $query->query['ep_integrate'] = true; - - if ( ! empty( $s ) ) { - - $searchable_post_types = $this->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; - } - } - } + return true; + } + /** + * Whether orders autosuggest is available or not + * + * @since 4.5.0 + * @return boolean + */ + public function is_orders_autosuggest_available() : bool { /** - * Set orderby and order for price/popularity when GET param not set + * Whether the autosuggest feature is available for non + * ElasticPress.io customers. + * + * @since 4.5.0 + * @hook ep_woocommerce_orders_autosuggest_available + * @param {boolean} $available Whether the feature is available. */ - 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; - } - } + return apply_filters( 'ep_woocommerce_orders_autosuggest_available', Utils\is_epio() ); + } - /** - * 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. - } - } + /** + * Whether orders autosuggest is enabled or not + * + * @since 4.5.0 + * @return boolean + */ + public function is_orders_autosuggest_enabled() : bool { + return $this->is_orders_autosuggest_available() && '1' === $this->get_setting( 'orders' ); } /** - * Fetch the ES related meta mapping for orderby + * 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 ) { - /** - * 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', + _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 + * + * @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 ) ) ); + } - if ( isset( $mapping[ $meta_key ] ) ) { - return $mapping[ $meta_key ]; - } + /** + * 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; + } - return 'date'; + /** + * 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; + } /** - * Returns the WooCommerce-oriented post types in admin that EP will search + * 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() { - $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 ); + _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(); } /** - * Make search coupons don't go through ES + * DEPRECATED. Make search coupons don't go through ES * * @param bool $enabled Coupons enabled or not * @param WP_Query $query WP Query @@ -621,19 +410,12 @@ public function get_admin_searchable_post_types() { * @return bool */ public function blacklist_coupons( $enabled, $query ) { - if ( is_admin() ) { - return $enabled; - } - - if ( 'shop_coupon' === $query->get( 'post_type' ) && empty( $query->query_vars['ep_integrate'] ) ) { - return false; - } - - return $enabled; + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->disallow_coupons()" ); + return $this->disallow_coupons( $enabled, $query ); } /** - * Allow order creations on the front end to get synced + * DEPRECATED. Allow order creations on the front end to get synced * * @since 2.1 * @param bool $override Original order perms check value @@ -641,17 +423,12 @@ public function blacklist_coupons( $enabled, $query ) { * @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; + _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 ); } /** - * Sets woocommerce meta search fields to an empty array if we are integrating the main query with ElasticSearch + * 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. @@ -661,29 +438,12 @@ public function bypass_order_permissions_check( $override, $post_id ) { * @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' ] ); + _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 ); } /** - * Enhance WooCommerce search order by order id, email, phone number, name, etc.. + * 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 @@ -693,295 +453,82 @@ public function maybe_hook_woocommerce_search_fields( $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 + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders->search_order()" ); + return $this->orders->search_order( $wp ); } /** - * Add order items as a searchable string. + * 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 ) { - $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 + * @since 2.4 + * + * @param array $post_args Post arguments + * @param string|int $post_id Post id + * + * @return array */ - 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 ); + 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 ); } /** - * Output feature box long + * DEPRECATED. Add WooCommerce Product Attributes to EP Facets. * - * @since 2.1 + * @param array $taxonomies Taxonomies array + * @return array */ - public function output_feature_box_long() { - ?> -

- get_registered_feature( 'woocommerce' )->products->add_product_attributes()" ); + return $this->products->add_product_attributes( $taxonomies ); } /** - * Dashboard WooCommerce settings + * DEPRECATED. Add WooCommerce Fields to the Weighting Dashboard. * - * @since 4.5.0 + * @since 3.x + * + * @param array $fields Current weighting fields. + * @param string $post_type Current post type. + * @return array New fields. */ - public function output_feature_box_settings() { - $available = $this->is_orders_autosuggest_available(); - $enabled = $this->is_orders_autosuggest_enabled(); - ?> -
-
-
-
- -

- tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ; */ - __( 'You are directly connected to %1$sElasticPress.io%2$s! Enable Orders Autosuggest to enhance Dashboard results and quickly find WooCommerce Orders. %3$sLearn More%4$s.', 'elasticpress' ) : - /* translators: 1: tag (ElasticPress.io); 2. ; 3: tag (KB article); 4. ; */ - __( 'Due to the sensitive nature of orders, this autosuggest feature is available only to %1$sElasticPress.io%2$s customers. %3$sLearn More%4$s.', 'elasticpress' ); - - printf( - wp_kses( $message, 'ep-html' ), - '', - '', - '', - '' - ); - ?> -

-
-
- get_registered_feature( 'woocommerce' )->products->add_product_attributes_to_weighting()" ); + return $this->products->add_product_attributes_to_weighting( $fields, $post_type ); } /** - * Remove the author_name from search fields. + * DEPRECATED. Add WooCommerce Fields to the default values of the Weighting Dashboard. * - * @param array $search_fields Array of search fields. - * @since 3.0 + * @since 3.x + * + * @param array $defaults Default values for the post type. + * @param string $post_type Current post type. * @return array */ - public function remove_author( $search_fields ) { - foreach ( $search_fields as $field_key => $field ) { - if ( 'author_name' === $field ) { - unset( $search_fields[ $field_key ] ); - } - } - - return $search_fields; + 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 ); } /** - * Determine WC feature reqs status + * DEPRECATED. Add WC post type to autosuggest * - * @since 2.2 - * @return EP_Feature_Requirements_Status + * @param array $post_types Array of post types (e.g. post, page). + * @since 2.6 + * @return array */ - public function requirements_status() { - $status = new FeatureRequirementsStatus( 0 ); - - if ( ! class_exists( 'WooCommerce' ) ) { - $status->code = 2; - $status->message = esc_html__( 'WooCommerce not installed.', 'elasticpress' ); - } - - return $status; + 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 ); } /** - * Modifies main query to allow filtering by price with WooCommerce "Filter by price" widget. + * 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 @@ -990,63 +537,12 @@ public function requirements_status() { * @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; + _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 ); } /** - * Prevent order fields from being removed. + * 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. @@ -1059,17 +555,12 @@ public function price_filter( $args, $query_args, $query ) { * @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; + _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 ); } /** - * Add a new `_variations_skus` meta field to the product to be indexed in Elasticsearch. + * 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 @@ -1077,39 +568,12 @@ public function keep_order_fields( $skip, $post_args ) { * @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; + _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 ); } /** - * Integrate ElasticPress with the WooCommerce Admin Product List. + * 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. @@ -1122,277 +586,57 @@ function ( $variations_skus, $current_id ) { * @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; + _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 ); } /** - * Apply the necessary changes to WP_Query in WooCommerce Admin Product List. + * 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 ) { - // 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 - * - * @param \WP_Query $query Query we might integrate with - * - * @return bool - */ - protected function should_integrate_with_query( $query ) { - // Lets make sure this doesn't interfere with the CLI - if ( defined( 'WP_CLI' ) && WP_CLI ) { - return false; - } - - if ( defined( 'WC_API_REQUEST' ) && WC_API_REQUEST ) { - return false; - } - - if ( isset( $query->query_vars['ep_integrate'] ) && ! filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN ) ) { - return false; - } - - /** - * Filter to skip WP Query integration - * - * @hook ep_skip_query_integration - * @param {bool} $skip True to skip - * @param {WP_Query} $query WP Query to evaluate - * @return {bool} New skip value - */ - if ( apply_filters( 'ep_skip_query_integration', false, $query ) ) { - return false; - } - - if ( ! Utils\is_integrated_request( $this->slug ) ) { - return false; - } - - /** - * Do nothing for single product queries - */ - $product_name = $query->get( 'product', false ); - if ( ! empty( $product_name ) || $query->is_single() ) { - return false; - } - - /** - * ElasticPress does not yet support post_parent queries - */ - $post_parent = $query->get( 'post_parent', false ); - if ( ! empty( $post_parent ) ) { - return false; - } - - /** - * If this is just a preview, let's not use Elasticsearch. - */ - if ( $query->get( 'preview', false ) ) { - return false; - } - - return true; + _deprecated_function( __METHOD__, '4.7.0', "\ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->products->price_filter()" ); + $this->products->translate_args_admin_products_list( $query ); } /** - * Depending on the number of products display an admin notice in the custom sort screen for WooCommerce Products + * 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 ) { - 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; + _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 ); } /** - * Conditionally resync products after applying a custom order. + * 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 ) { - - $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 - * - * @since 4.5.0 - * @return boolean - */ - public function is_orders_autosuggest_available() : bool { - /** - * Whether the autosuggest feature is available for non - * ElasticPress.io customers. - * - * @since 4.5.0 - * @hook ep_woocommerce_orders_autosuggest_available - * @param {boolean} $available Whether the feature is available. - */ - return apply_filters( 'ep_woocommerce_orders_autosuggest_available', Utils\is_epio() ); - } - - /** - * Whether orders autosuggest is enabled or not - * - * @since 4.5.0 - * @return boolean - */ - public function is_orders_autosuggest_enabled() : bool { - return $this->is_orders_autosuggest_available() && '1' === $this->get_setting( '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 ); } /** - * Add weight by date settings related to WooCommerce + * 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 +645,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 ); } - } diff --git a/includes/classes/StatusReport/ElasticPressIo.php b/includes/classes/StatusReport/ElasticPressIo.php index 4ebd236952..31f67f6096 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/WooCommerce/TestWooCommerce.php b/tests/php/features/WooCommerce/TestWooCommerce.php new file mode 100644 index 0000000000..886f90ba68 --- /dev/null +++ b/tests/php/features/WooCommerce/TestWooCommerce.php @@ -0,0 +1,357 @@ +suppress_errors(); + + $admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); + + wp_set_current_user( $admin_id ); + + ElasticPress\Elasticsearch::factory()->delete_all_indices(); + ElasticPress\Indexables::factory()->get( 'post' )->put_mapping(); + + ElasticPress\Indexables::factory()->get( 'post' )->sync_manager->sync_queue = []; + + $this->setup_test_post_type(); + } + + /** + * Clean up after each test. Reset our mocks + * + * @since 2.1 + * @group woocommerce + */ + public function tear_down() { + parent::tear_down(); + + $this->fired_actions = array(); + } + + /** + * Test search integration is on in general for product searches + * + * @since 2.1 + * @group woocommerce + */ + 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 ); + } + + /** + * Tests the search query for a shop_coupon. + * + * @since 4.4.1 + * @group woocommerce + */ + 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 + * @group woocommerce + */ + 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 + * @group woocommerce + */ + 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 the `is_orders_autosuggest_available` method + * + * @since 4.5.0 + * @group woocommerce + */ + 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 + * @group woocommerce + */ + 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 the addition of variations skus to product meta + * + * @since 4.2.0 + * @group woocommerce + * @expectedDeprecated ElasticPress\Feature\WooCommerce\WooCommerce::add_variations_skus_meta + */ + public function testAddVariationsSkusMeta() { + ElasticPress\Features::factory()->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' ) + ->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'] ); + } + + /** + * Test the translate_args_admin_products_list method + * + * @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' ); + 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, '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 + * + * @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' ); + 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, '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/TestWooCommerceOrders.php b/tests/php/features/WooCommerce/TestWooCommerceOrders.php new file mode 100644 index 0000000000..9c1eff486e --- /dev/null +++ b/tests/php/features/WooCommerce/TestWooCommerceOrders.php @@ -0,0 +1,334 @@ +orders = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' )->orders; + } + + /** + * 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. + * + * If searching for a number that is an order ID and part of another order's metadata, + * both should be returned. + * + * @group woocommerce + * @group woocommerce-orders + */ + 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 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 + * + * @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}" ); + $this->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/TestWooCommerceOrders.php b/tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php similarity index 80% rename from tests/php/features/TestWooCommerceOrders.php rename to tests/php/features/WooCommerce/TestWooCommerceOrdersAutosuggest.php index 1160d78124..de3682e8d6 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,15 @@ 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 woocommerce + * @group woocommerce-orders-autosuggest */ public function set_up() { parent::set_up(); @@ -45,13 +46,14 @@ 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 woocommerce + * @group woocommerce-orders-autosuggest */ public function testFilterTermSuggest() { $order = [ @@ -70,7 +72,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 +88,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 +106,8 @@ public function testFilterTermSuggest() { * * This method steps into WooCommerce functionality a bit. * - * @group WooCommerceOrders + * @group woocommerce + * @group woocommerce-orders-autosuggest */ public function testFilterTermSuggestWithCustomOrderId() { $shop_order_1 = new \WC_Order(); @@ -115,7 +118,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 +136,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 +150,8 @@ public function testFilterTermSuggestWithCustomOrderId() { /** * Test the `mapping` method with the ES 7 mapping * - * @group WooCommerceOrders + * @group woocommerce + * @group woocommerce-orders-autosuggest */ public function testMappingEs7() { $original_mapping = [ @@ -157,7 +161,7 @@ public function testMappingEs7() { ], ], ]; - $changed_mapping = $this->orders->mapping( $original_mapping ); + $changed_mapping = $this->orders_autosuggest->mapping( $original_mapping ); $expected_mapping = [ 'mappings' => [ @@ -192,7 +196,8 @@ public function testMappingEs7() { /** * Test the `mapping` method with the ES 5 mapping * - * @group WooCommerceOrders + * @group woocommerce + * @group woocommerce-orders-autosuggest */ public function testMappingEs5() { $change_es_version = function() { @@ -210,7 +215,7 @@ public function testMappingEs5() { ], ]; - $changed_mapping = $this->orders->mapping( $original_mapping ); + $changed_mapping = $this->orders_autosuggest->mapping( $original_mapping ); $expected_mapping = [ 'mappings' => [ @@ -247,7 +252,8 @@ public function testMappingEs5() { /** * Test the `set_search_fields` method * - * @group WooCommerceOrders + * @group woocommerce + * @group woocommerce-orders-autosuggest */ public function testSetSearchFields() { $original_search_fields = [ 'old_search_field' ]; @@ -261,7 +267,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 +280,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', diff --git a/tests/php/features/TestWooCommerce.php b/tests/php/features/WooCommerce/TestWooCommerceProduct.php similarity index 67% rename from tests/php/features/TestWooCommerce.php rename to tests/php/features/WooCommerce/TestWooCommerceProduct.php index 8e5be36887..bb1fda8696 100644 --- a/tests/php/features/TestWooCommerce.php +++ b/tests/php/features/WooCommerce/TestWooCommerceProduct.php @@ -1,7 +1,8 @@ suppress_errors(); - - $admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); - - wp_set_current_user( $admin_id ); - - ElasticPress\Elasticsearch::factory()->delete_all_indices(); - ElasticPress\Indexables::factory()->get( 'post' )->put_mapping(); - - ElasticPress\Indexables::factory()->get( 'post' )->sync_manager->sync_queue = []; - - $this->setup_test_post_type(); - } + protected $products; /** - * Clean up after each test. Reset our mocks + * Setup each test. * - * @since 2.1 * @group woocommerce + * @group woocommerce-orders */ - public function tear_down() { - parent::tear_down(); - - $this->fired_actions = array(); + 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 * - * @since 2.1 * @group woocommerce + * @group woocommerce-products */ public function testProductsPostTypeQueryOn() { ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); @@ -82,8 +65,8 @@ public function testProductsPostTypeQueryOn() { /** * Test products post type query does get integrated when querying WC product_cat taxonomy * - * @since 2.1 * @group woocommerce + * @group woocommerce-products */ public function testProductsPostTypeQueryProductCatTax() { ElasticPress\Features::factory()->activate_feature( 'admin' ); @@ -107,209 +90,20 @@ public function testProductsPostTypeQueryProductCatTax() { $query = new \WP_Query( $args ); $this->assertTrue( $query->elasticsearch_success ); - } - - /** - * Test search integration is on for shop orders - * - * @since 2.1 - * @group woocommerce - */ - 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. - * - * @since 4.5 - */ - 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. - * - * @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', - ) - ); - - 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. - * - * @since 4.5 - */ - 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 - * - * @since 4.0.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(); - $args = array( - 's' => (string) $shop_order_id, - 'post_type' => 'shop_order', - ); + $args = [ 'product_cat' => 'cat' ]; $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. - * - * 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 * - * @since 2.1 * @group woocommerce + * @group woocommerce-products */ public function testSearchOnAllFrontEnd() { ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); @@ -327,229 +121,11 @@ public function testSearchOnAllFrontEnd() { $this->assertTrue( $query->elasticsearch_success ); } - /** - * Test the addition of variations skus to product meta - * - * @since 4.2.0 - * @group woocommerce - */ - public function testAddVariationsSkusMeta() { - ElasticPress\Features::factory()->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' ) - ->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'] ); - } - - /** - * Test the translate_args_admin_products_list method - * - * @since 4.2.0 - * @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, '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 - * - * @since 4.2.0 - * @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, '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' ] - ); - } - - /** - * 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 + * @group woocommerce + * @group woocommerce-products */ public function testWoocommerceAttributeTaxonomiesAreSync() { ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); @@ -686,7 +262,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 @@ -694,7 +270,8 @@ public function productQueryOrderDataProvider() : array { * @param array $expected Value expected * @param string $order Order * @dataProvider productQueryOrderDataProvider - * @since 4.5.0 + * @group woocommerce + * @group woocommerce-products */ public function testProductQueryOrder( $product_arg_key, $query_args, $query_string, $expected, $order = '' ) { global $wp_the_query; @@ -764,7 +341,8 @@ function ( $formatted_args ) use ( $expected ) { /** * Test the product query not use Elasticsearch if preview. * - * @since 4.5.0 + * @group woocommerce + * @group woocommerce-products */ public function testQueryShouldNotUseElasticsearchIfPreview() { ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); @@ -780,10 +358,12 @@ public function testQueryShouldNotUseElasticsearchIfPreview() { $this->assertNull( $query->elasticsearch_success ); } + /** * Test that on Admin Product List use Elasticsearch. * - * @since 4.5.0 + * @group woocommerce + * @group woocommerce-products */ public function testProductListInAdminUseElasticSearch() { global $typenow, $wc_list_table; @@ -830,7 +410,8 @@ function( $filters, $args, $query ) { /** * Test that Search in Admin Product List use Elasticsearch. * - * @since 4.5.0 + * @group woocommerce + * @group woocommerce-products */ public function testProductListSearchInAdminUseElasticSearch() { global $typenow, $wc_list_table; @@ -882,7 +463,8 @@ function ( $formatted_args, $args, $wp_query ) { /** * Test the product query when price filter is set. * - * @since 4.5.0 + * @group woocommerce + * @group woocommerce-products */ public function testPriceFilter() { global $wp_the_query, $wp_query; @@ -951,7 +533,8 @@ function ( $formatted_args ) { /** * Test the product search query when price filter is set. * - * @since 4.5.0 + * @group woocommerce + * @group woocommerce-products */ public function testPriceFilterWithSearchQuery() { global $wp_the_query, $wp_query; @@ -1010,7 +593,8 @@ public function testPriceFilterWithSearchQuery() { /** * Tests that attributes filter uses Elasticsearch. * - * @since 4.5.0 + * @group woocommerce + * @group woocommerce-products */ public function testAttributesFilterUseES() { global $wp_the_query; @@ -1066,7 +650,8 @@ function( \WP_Query $query ) { /** * Tests that get_posts() uses Elasticsearch when ep_integrate is true. * - * @since 4.5.0 + * @group woocommerce + * @group woocommerce-products */ public function testGetPosts() { ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); @@ -1089,7 +674,8 @@ public function testGetPosts() { /** * Tests that get_posts() does not use Elasticsearch when ep_integrate is not set. * - * @since 4.5.0 + * @group woocommerce + * @group woocommerce-products */ public function testGetPostQueryDoesNotUseElasticSearchByDefault() { ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); @@ -1112,7 +698,7 @@ public function testGetPostQueryDoesNotUseElasticSearchByDefault() { /** * Tests that Weighting dashboard shows SKU and Variation SKUs option. * - * @since 4.5.0 + * @group woocommerce */ public function testSkuOptionAddInWeightDashboard() { ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); @@ -1132,68 +718,123 @@ public function testSkuOptionAddInWeightDashboard() { } /** - * Test the `is_orders_autosuggest_available` method + * Test the addition of variations skus to product meta * - * @since 4.5.0 + * @group woocommerce + * @group woocommerce-products */ - public function testIsOrdersAutosuggestAvailable() { - $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); + public function testAddVariationsSkusMeta() { + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); - $this->assertSame( $woocommerce_feature->is_orders_autosuggest_available(), \ElasticPress\Utils\is_epio() ); + $this->assertTrue( class_exists( '\WC_Product_Variable' ) ); + $this->assertTrue( class_exists( '\WC_Product_Variation' ) ); - /** - * 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() ); + $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 ); - add_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_false' ); - $this->assertFalse( $woocommerce_feature->is_orders_autosuggest_available() ); + $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'] ); } /** - * Test the `is_orders_autosuggest_available` method + * Test the translate_args_admin_products_list method * - * @since 4.5.0 + * @group woocommerce + * @group woocommerce-products */ - public function testIsOrdersAutosuggestEnabled() { - $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); + public function testTranslateArgsAdminProductsList() { + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); - $this->assertFalse( $woocommerce_feature->is_orders_autosuggest_enabled() ); + parse_str( 'post_type=product&s=product&product_type=downloadable&stock_status=instock', $_GET ); - /** - * 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_args = [ + 'ep_integrate' => true, + ]; - /** - * Enable it - */ - $filter = function() { - return [ - 'woocommerce' => [ - 'orders' => '1', + $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 + * @group woocommerce-products + */ + 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( 'pre_site_option_ep_feature_settings', $filter ); - add_filter( 'pre_option_ep_feature_settings', $filter ); - $this->assertTrue( $woocommerce_feature->is_orders_autosuggest_enabled() ); + add_filter( 'ep_woocommerce_admin_products_list_search_fields', $search_fields_function ); - /** - * 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() ); + $query = new \WP_Query( $query_args ); + $this->assertEquals( + $query->query_vars['search_fields'], + [ 'post_title', 'post_content' ] + ); } /** * Test if decaying is disabled on products. * - * @since 4.6.0 * @dataProvider decayingDisabledOnProductsProvider * @group woocommerce + * @group woocommerce-products * * @param string $setting Value for `decaying_enabled` * @param array|string $post_type Post types to be queried @@ -1225,7 +866,6 @@ public function testDecayingDisabledOnProducts( $setting, $post_type, $assert ) /** * Data provider for the testDecayingDisabledOnProducts method. * - * @since 4.6.0 * @return array */ public function decayingDisabledOnProductsProvider() : array { @@ -1267,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' ); + } }