diff --git a/CHANGELOG.md b/CHANGELOG.md index 51d3dd5f65..eb0c86ca96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,27 @@ All notable changes to this project will be documented in this file, per [the Ke ### Security --> +## [4.6.1] - 2023-07-05 + +**Note that starting from the ElasticPress 5.0.0 release the `Users` feature will be moved to the [ElasticPress Labs](https://github.com/10up/ElasticPressLabs) plugin. The `Terms` and `Comments` features will remain in ElasticPress but will be available only if enabled via code. Check [our blog post](https://www.elasticpress.io/blog/2023/03/enabling-comments-and-terms-in-elasticpress-5-0) for more info.** + +### Added +* Add doc url for "Did You Mean" feature. Props [@burhandodhy](https://github.com/burhandodhy) via [#3529](https://github.com/10up/ElasticPress/pull/3529). + +### Changed +* Use `wp_cache_supports` over `wp_cache_supports_group_flush`. Props [@spacedmonkey](https://github.com/spacedmonkey) via [#3501](https://github.com/10up/ElasticPress/pull/3501). +* Update the `ep_exclude_from_search` post meta only if it is set or has some value. Props [@MARQAS](https://github.com/MARQAS) and [@columbian-chris](https://github.com/columbian-chris) via [#3521](https://github.com/10up/ElasticPress/pull/3521). + +### Fixed +* Deprecation notice in `ElasticPress\Feature\WooCommerce\Orders`. Props [@mwidmann](https://github.com/mwidmann) via [#3507](https://github.com/10up/ElasticPress/pull/3507). +* Don't apply a facet filter to the query if the filter value is empty. Props [@burhandodhy](https://github.com/burhandodhy) via [#3524](https://github.com/10up/ElasticPress/pull/3524). +* Syncing a post with empty post meta key. Props [@MARQAS](https://github.com/MARQAS) and [@oscarssanchez](https://github.com/oscarssanchez) via [#3516](https://github.com/10up/ElasticPress/pull/3516). +* Order by clauses with Elasticsearch field formats are not changed anymore. Props [@felipeelia](https://github.com/felipeelia) and [@tlovett1](https://github.com/tlovett1) via [#3512](https://github.com/10up/ElasticPress/pull/3512). +* Failed Query logs are automatically cleared on refreshing the "Status Report" page. Props [@burhandodhy](https://github.com/burhandodhy) via [#3533](https://github.com/10up/ElasticPress/pull/3533). +* Warning message on Health page when Last Sync information is not available. Props [@burhandodhy](https://github.com/burhandodhy) via [#3530](https://github.com/10up/ElasticPress/pull/3530). +* Deprecation notice: json_encode(): Passing null to parameter #2. Props [@burhandodhy](https://github.com/burhandodhy) via [#3532](https://github.com/10up/ElasticPress/pull/3532). +* Documentation of the `ep_facet_search_get_terms_args` filter. Props [@burhandodhy](https://github.com/burhandodhy) via [#3525](https://github.com/10up/ElasticPress/pull/3525). + ## [4.6.0] - 2023-06-13 **Note that starting from the ElasticPress 5.0.0 release the `Users` feature will be moved to the [ElasticPress Labs](https://github.com/10up/ElasticPressLabs) plugin. The `Terms` and `Comments` features will remain in ElasticPress but will be available only if enabled via code. Check [our blog post](https://www.elasticpress.io/blog/2023/03/enabling-comments-and-terms-in-elasticpress-5-0) for more info.** @@ -1831,6 +1852,7 @@ This is a bug fix release with some filter additions. - Initial plugin release [Unreleased]: https://github.com/10up/ElasticPress/compare/trunk...develop +[4.6.1]: https://github.com/10up/ElasticPress/compare/4.6.0...4.6.1 [4.6.0]: https://github.com/10up/ElasticPress/compare/4.5.2...4.6.0 [4.5.2]: https://github.com/10up/ElasticPress/compare/4.5.1...4.5.2 [4.5.1]: https://github.com/10up/ElasticPress/compare/4.5.0...4.5.1 diff --git a/CREDITS.md b/CREDITS.md index d0bb519dc3..96d60613a2 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -213,6 +213,7 @@ Thank you to all the people who have already contributed to this repository via [Alexander Z (@TorlockC)](https://github.com/TorlockC), [Nabi (@navidabdi)](https://github.com/navidabdi) [Dmitry Seleznyov (@selim13)](https://github.com/selim13) +[Martin Widmann (@mwidmann)](https://github.com/mwidmann) and [@qazaqstan2025](https://github.com/qazaqstan2025). diff --git a/elasticpress.php b/elasticpress.php index 0b90abe5dc..afd0d1335b 100644 --- a/elasticpress.php +++ b/elasticpress.php @@ -3,7 +3,7 @@ * Plugin Name: ElasticPress * Plugin URI: https://github.com/10up/ElasticPress * Description: A fast and flexible search and query engine for WordPress. - * Version: 4.6.0 + * Version: 4.6.1 * Requires at least: 5.6 * Requires PHP: 7.0 * Author: 10up @@ -32,7 +32,7 @@ define( 'EP_URL', plugin_dir_url( __FILE__ ) ); define( 'EP_PATH', plugin_dir_path( __FILE__ ) ); define( 'EP_FILE', plugin_basename( __FILE__ ) ); -define( 'EP_VERSION', '4.6.0' ); +define( 'EP_VERSION', '4.6.1' ); /** * PSR-4-ish autoloading diff --git a/includes/classes/Command.php b/includes/classes/Command.php index 5a5e510b13..744970ae5a 100644 --- a/includes/classes/Command.php +++ b/includes/classes/Command.php @@ -1484,31 +1484,6 @@ public function settings_reset( $args, $assoc_args ) { WP_CLI::line( esc_html__( 'Settings deleted.', 'elasticpress' ) ); } - /** - * Get an index settings - * - * ## OPTIONS - * - * - * : Index name - * - * [--pretty] - * : Use this flag to render a pretty-printed version of the JSON response. - * - * @subcommand get-index-settings - * - * @since 4.7.0 - * - * @param array $args Positional CLI args. - * @param array $assoc_args Associative CLI args. - */ - public function get_index_settings( $args, $assoc_args ) { - $response = Elasticsearch::factory()->remote_request( $args[0] ); - $pretty = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pretty' ); - - $this->print_json_response( $response, $pretty ); - } - /** * Print an HTTP response. * @@ -1540,7 +1515,7 @@ protected function print_json_response( $response, $pretty ) { * @param boolean $pretty_print_flag Whether it should or not be formatted. */ protected function pretty_json_encode( $json_obj, $pretty_print_flag ) { - $flag = $pretty_print_flag ? JSON_PRETTY_PRINT : null; + $flag = $pretty_print_flag ? JSON_PRETTY_PRINT : 0; WP_CLI::line( wp_json_encode( $json_obj, $flag ) ); } @@ -1589,4 +1564,29 @@ public function delete_search_template() { $instant_results->epio_delete_search_template(); WP_CLI::success( esc_html__( 'Done.', 'elasticpress' ) ); } + + /** + * Get an index settings + * + * ## OPTIONS + * + * + * : Index name + * + * [--pretty] + * : Use this flag to render a pretty-printed version of the JSON response. + * + * @subcommand get-index-settings + * + * @since 4.7.0 + * + * @param array $args Positional CLI args. + * @param array $assoc_args Associative CLI args. + */ + public function get_index_settings( $args, $assoc_args ) { + $response = Elasticsearch::factory()->get_index_settings( $args[0] ); + $pretty = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pretty' ); + + $this->pretty_json_encode( $response, $pretty ); + } } diff --git a/includes/classes/Feature/Autosuggest/Autosuggest.php b/includes/classes/Feature/Autosuggest/Autosuggest.php index 7a474c213d..b62c43ae86 100644 --- a/includes/classes/Feature/Autosuggest/Autosuggest.php +++ b/includes/classes/Feature/Autosuggest/Autosuggest.php @@ -660,7 +660,7 @@ public function intercept_search_request( $response, $query = [], $args = [], $f public function delete_cached_query() { global $wp_object_cache; if ( wp_using_ext_object_cache() ) { - if ( function_exists( 'wp_cache_supports_group_flush' ) && wp_cache_supports_group_flush() ) { + if ( function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_group' ) ) { wp_cache_flush_group( 'ep_autosuggest' ); } else { // Try to delete the entire group. diff --git a/includes/classes/Feature/DidYouMean/DidYouMean.php b/includes/classes/Feature/DidYouMean/DidYouMean.php index 86f4f9b154..00228d1c19 100644 --- a/includes/classes/Feature/DidYouMean/DidYouMean.php +++ b/includes/classes/Feature/DidYouMean/DidYouMean.php @@ -25,6 +25,8 @@ public function __construct() { $this->summary = __( 'Recommend alternative search terms for misspelled queries or terms with no results.', 'elasticpress' ); + $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/16673223107085-Did-You-Mean', 'elasticpress' ); + $this->requires_install_reindex = true; $this->available_during_installation = true; diff --git a/includes/classes/Feature/Facets/Facets.php b/includes/classes/Feature/Facets/Facets.php index 58760a3198..48e6c24a98 100644 --- a/includes/classes/Feature/Facets/Facets.php +++ b/includes/classes/Feature/Facets/Facets.php @@ -401,6 +401,9 @@ public function get_selected() { foreach ( $filter_names as $filter_name => $type_obj ) { if ( 0 === strpos( $key, $filter_name ) ) { + if ( empty( $value ) ) { + continue; + } $facet = str_replace( $filter_name, '', $key ); $filters = $type_obj->format_selected( $facet, $value, $filters ); diff --git a/includes/classes/Feature/Facets/Types/Taxonomy/Renderer.php b/includes/classes/Feature/Facets/Types/Taxonomy/Renderer.php index 97f50cf0bd..7a6e32304d 100644 --- a/includes/classes/Feature/Facets/Types/Taxonomy/Renderer.php +++ b/includes/classes/Feature/Facets/Types/Taxonomy/Renderer.php @@ -95,10 +95,10 @@ public function render( $args, $instance ) { * * @since 3.5.0 * @hook ep_facet_search_get_terms_args - * @param {array} $query Weighting query - * @param {string} $post_type Post type - * @param {array} $args WP Query arguments - * @return {array} New query + * @param {array} $terms_args Array of arguments passed to get_terms() + * @param {array} $args Widget args + * @param {array} $instance Instance settings + * @return {array} New terms args */ apply_filters( 'ep_facet_search_get_terms_args', diff --git a/includes/classes/Feature/Search/Search.php b/includes/classes/Feature/Search/Search.php index 736d71303f..a0cd3ffb76 100644 --- a/includes/classes/Feature/Search/Search.php +++ b/includes/classes/Feature/Search/Search.php @@ -110,8 +110,11 @@ public function search_setup() { add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue_block_editor_assets' ] ); add_filter( 'ep_post_filters', [ $this, 'exclude_posts_from_search' ], 10, 3 ); add_action( 'post_submitbox_misc_actions', [ $this, 'output_exclude_from_search_setting' ] ); - add_action( 'edit_post', [ $this, 'save_exclude_from_search_meta' ], 10, 2 ); + add_action( 'edit_post', [ $this, 'save_exclude_from_search_meta' ] ); add_filter( 'ep_skip_query_integration', [ $this, 'skip_query_integration' ], 10, 2 ); + + add_action( 'attachment_submitbox_misc_actions', [ $this, 'output_exclude_from_search_setting' ], 15 ); + add_action( 'edit_attachment', [ $this, 'save_exclude_from_search_meta' ] ); } @@ -775,7 +778,6 @@ public function exclude_posts_from_search( $filters, $args, $query ) { * @param WP_POST $post Post object. */ public function output_exclude_from_search_setting( $post ) { - $searchable_post_types = $this->get_searchable_post_types(); if ( ! in_array( $post->post_type, $searchable_post_types, true ) ) { return; @@ -784,7 +786,13 @@ public function output_exclude_from_search_setting( $post ) {
> -

+

+ post_type ) : ?> + + + + +

index = Indexables::factory()->get( 'post' )->get_index_name(); - } + protected $woocommerce; /** - * Setup feature functionality. + * Class constructor * - * @return void + * @param WooCommerce $woocommerce WooCommerce feature object instance */ - 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 ); + public function __construct( WooCommerce $woocommerce ) { + $this->woocommerce = $woocommerce; } /** - * 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. + * Setup order related hooks */ - public function get_template_endpoint() { - /** - * Filters the WooCommerce Orders search template API endpoint. - * - * @since 4.5.0 - * @hook ep_woocommerce_order_search_template_endpoint - * @param {string} $endpoint Endpoint path. - * @param {string} $index Elasticsearch index. - * @returns {string} Search template API endpoint. - */ - return apply_filters( 'ep_woocommerce_order_search_template_endpoint', "api/v1/search/orders/{$this->index}/template", $this->index ); - } - - /** - * Get the endpoint for temporary tokens. - * - * @return string Temporary token endpoint. - */ - public function get_token_endpoint() { - /** - * Filters the temporary token API endpoint. - * - * @since 4.5.0 - * @hook ep_token_endpoint - * @param {string} $endpoint Endpoint path. - * @returns {string} Token API endpoint. - */ - return apply_filters( 'ep_token_endpoint', 'api/v1/token' ); - } - - /** - * Registers the API endpoint to get a token. - * - * @return void - */ - public function rest_api_init() { - register_rest_route( - 'elasticpress/v1', - 'token', - [ - [ - 'callback' => [ $this, 'get_token' ], - 'permission_callback' => [ $this, 'check_token_permission' ], - 'methods' => 'GET', - ], - [ - 'callback' => [ $this, 'refresh_token' ], - 'permission_callback' => [ $this, 'check_token_permission' ], - 'methods' => 'POST', - ], - ] - ); + public function setup() { + add_filter( 'ep_sync_insert_permissions_bypass', [ $this, 'bypass_order_permissions_check' ], 10, 2 ); + add_filter( 'ep_prepare_meta_allowed_protected_keys', [ $this, 'allow_meta_keys' ] ); + add_filter( 'ep_post_sync_args_post_prepare_meta', [ $this, 'add_order_items_search' ], 20, 2 ); + add_filter( 'ep_pc_skip_post_content_cleanup', [ $this, 'keep_order_fields' ], 20, 2 ); + add_action( 'parse_query', [ $this, 'maybe_hook_woocommerce_search_fields' ], 1 ); + add_action( 'parse_query', [ $this, 'search_order' ], 11 ); + add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 ); } /** - * Enqueue admin assets. + * Allow order creations on the front end to get synced * - * @param string $hook_suffix The current admin page. + * @param bool $override Original order perms check value + * @param int $post_id Post ID + * @return bool */ - 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 bypass_order_permissions_check( $override, $post_id ) { + $searchable_post_types = $this->get_admin_searchable_post_types(); - /** - * 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 ( in_array( get_post_type( $post_id ), $searchable_post_types, true ) ) { + return true; } - if ( true === $data['active'] ) { - $this->epio_save_search_template(); - } else { - $this->epio_delete_search_template(); - } + return $override; } /** - * Save the search template to ElasticPress.io. + * Returns the WooCommerce-oriented post types in admin that EP will search * - * @return void + * @return array */ - 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', - ] - ); + public function get_admin_searchable_post_types() { + $searchable_post_types = array( 'shop_order' ); /** - * Fires after the request is sent the search template API endpoint. + * Filter admin searchable WooCommerce post types * - * @since 4.5.0 - * @hook ep_woocommerce_order_search_template_saved - * @param {string} $template The search template (JSON). - * @param {string} $index Index name. + * @hook ep_woocommerce_admin_searchable_post_types + * @since 4.4.0 + * @param {array} $post_types Post types + * @return {array} New post types */ - do_action( 'ep_woocommerce_order_search_template_saved', $template, $this->index ); + return apply_filters( 'ep_woocommerce_admin_searchable_post_types', $searchable_post_types ); } /** - * Delete the search template from ElasticPress.io. + * Index WooCommerce orders meta fields * - * @return void + * @param array $meta Existing post meta + * @return array */ - public function epio_delete_search_template() { - $endpoint = $this->get_template_endpoint(); - - Elasticsearch::factory()->remote_request( - $endpoint, - [ - 'blocking' => false, - 'method' => 'DELETE', - ] + 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', + ) + ) ); - - /** - * 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. + * 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". * - * @return string|WP_Error Search template if found, WP_Error on error. + * @param array $post_args Post arguments + * @param string|int $post_id Post id + * + * @return array */ - public function epio_get_search_template() { - $endpoint = $this->get_template_endpoint(); - $request = Elasticsearch::factory()->remote_request( $endpoint ); + public function add_order_items_search( $post_args, $post_id ) { + $searchable_post_types = $this->get_admin_searchable_post_types(); - if ( is_wp_error( $request ) ) { - return $request; + // Make sure it is only WooCommerce orders we touch. + if ( ! in_array( $post_args['post_type'], $searchable_post_types, true ) ) { + return $post_args; } - $response = wp_remote_retrieve_body( $request ); - - return $response; - } + $post_indexable = Indexables::factory()->get( 'post' ); - /** - * 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}}', - ) - ); + // 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']; + } + } - 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 ); + // 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 $this->search_template; + return $post_args; } /** - * Return true if a given feature is supported by WooCommerce Orders. + * Prevent order fields from being removed. * - * 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. + * 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. * - * @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. + * @see https://github.com/10up/ElasticPress/issues/2726 * - * @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 + * @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 intercept_search_request( $response, $query = [], $args = [], $failures = 0 ) { - $this->search_template = $query['args']['body']; + public function keep_order_fields( $skip, $post_args ) { + $searchable_post_types = $this->get_admin_searchable_post_types(); - 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' => '', - ), - ); + if ( in_array( $post_args['post_type'], $searchable_post_types, true ) ) { + return true; + } - return $args; + return $skip; } /** - * Get a temporary token. + * Sets woocommerce meta search fields to an empty array if we are integrating the main query with ElasticSearch * - * @return string|false Authorization header, or false on failure. + * Woocommerce calls this action as part of its own callback on parse_query. We add this filter only if the query + * is integrated with ElasticSearch. + * If we were to always return array() on this filter, we'd break admin searches when WooCommerce module is activated + * without the Protected Content Module + * + * @param \WP_Query $query Current query */ - public function 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..08ffda2396 --- /dev/null +++ b/includes/classes/Feature/WooCommerce/Products.php @@ -0,0 +1,998 @@ +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. + * + * A product-related query will be integrated if: + * * Is the main query OR is a search OR has `ep_integrate` set as true + * * Is querying a supported taxonomy like product attributes + * * Is querying a supported post type like `product` + * + * @param \WP_Query $query Query we might integrate with + * @return bool + */ + public function should_integrate_with_query( \WP_Query $query ) : bool { + $has_ep_integrate = isset( $query->query_vars['ep_integrate'] ) && filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN ); + $is_search = '' !== $this->woocommerce->get_search_term( $query ); + + if ( ! $query->is_main_query() && ! $is_search && ! $has_ep_integrate ) { + return false; + } + + /** + * 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/Indexable/Post/Post.php b/includes/classes/Indexable/Post/Post.php index e3116739fa..663154da04 100644 --- a/includes/classes/Indexable/Post/Post.php +++ b/includes/classes/Indexable/Post/Post.php @@ -936,7 +936,9 @@ public function prepare_meta( $post ) { $prepared_meta = []; foreach ( $filtered_metas as $key => $value ) { - $prepared_meta[ $key ] = maybe_unserialize( $value ); + if ( ! empty( $key ) ) { + $prepared_meta[ $key ] = maybe_unserialize( $value ); + } } /** @@ -1290,6 +1292,12 @@ protected function parse_orderby_meta_fields( $orderby_clause, $args ) { 'unsigned' => 'long', ]; + // Code is targeting Elasticsearch directly + if ( preg_match( '/^meta\.(.*?)\.(.*)/', $orderby_clause, $match_meta ) ) { + return $orderby_clause; + } + + // WordPress meta_value_* compatibility if ( preg_match( '/^meta_value_?(.*)/', $orderby_clause, $match_type ) ) { $meta_type = $from_to_metatypes[ strtolower( $match_type[1] ) ] ?? 'value.sortable'; } @@ -1298,25 +1306,48 @@ protected function parse_orderby_meta_fields( $orderby_clause, $args ) { $meta_field = $args['meta_key']; } - if ( ( ! isset( $meta_type ) || ! isset( $meta_field ) ) && ! empty( $args['meta_query'] ) ) { - $meta_query = new \WP_Meta_Query( $args['meta_query'] ); - // Calling get_sql() to populate the WP_Meta_Query->clauses attribute - $meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); + // Already have everything needed + if ( isset( $meta_type ) && isset( $meta_field ) ) { + return "meta.{$meta_field}.{$meta_type}"; + } + + // Don't have any other ways to guess + if ( empty( $args['meta_query'] ) ) { + return $orderby_clause; + } - $clauses = $meta_query->get_clauses(); + $meta_query = new \WP_Meta_Query( $args['meta_query'] ); + // Calling get_sql() to populate the WP_Meta_Query->clauses attribute + $meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); - if ( ! empty( $clauses[ $orderby_clause ] ) ) { - $meta_field = $clauses[ $orderby_clause ]['key']; - $clause_meta_type = strtolower( $clauses[ $orderby_clause ]['type'] ?? $clauses[ $orderby_clause ]['cast'] ); + $clauses = $meta_query->get_clauses(); + + // If it refers to a named meta_query clause + if ( ! empty( $clauses[ $orderby_clause ] ) ) { + $meta_field = $clauses[ $orderby_clause ]['key']; + $clause_meta_type = strtolower( $clauses[ $orderby_clause ]['type'] ?? $clauses[ $orderby_clause ]['cast'] ); + } else { + /** + * At this point we: + * 1. Try to find the meta key in any meta_query clause and use the type WP found + * 2. If ordering by `meta_value*`, use the first meta_query clause + * 3. Give up and use the orderby clause as is (code could be capturing it later on) + */ + $meta_keys_and_types = wp_list_pluck( $clauses, 'cast', 'key' ); + if ( isset( $meta_keys_and_types[ $orderby_clause ] ) ) { + $meta_field = $orderby_clause; + $clause_meta_type = strtolower( $meta_keys_and_types[ $orderby_clause ] ?? $meta_keys_and_types[ $orderby_clause ] ); + } elseif ( isset( $meta_type ) ) { + $primary_clause = reset( $clauses ); + $meta_field = $primary_clause['key']; } else { - $primary_clause = reset( $clauses ); - $meta_field = $primary_clause['key']; - $clause_meta_type = strtolower( $primary_clause['type'] ?? $primary_clause['cast'] ); + unset( $meta_type ); + unset( $meta_field ); } + } - if ( ! isset( $meta_type ) ) { - $meta_type = $from_to_metatypes[ $clause_meta_type ] ?? 'value.sortable'; - } + if ( ! isset( $meta_type ) && isset( $clause_meta_type ) ) { + $meta_type = $from_to_metatypes[ $clause_meta_type ] ?? 'value.sortable'; } if ( isset( $meta_type ) && isset( $meta_field ) ) { diff --git a/includes/classes/Screen/HealthInfo.php b/includes/classes/Screen/HealthInfo.php index ca4a6ac1dd..166799dc7a 100644 --- a/includes/classes/Screen/HealthInfo.php +++ b/includes/classes/Screen/HealthInfo.php @@ -37,7 +37,7 @@ public function last_sync_health_info( $debug_info ) { $debug_info['ep-last-sync'] = [ 'label' => esc_html__( 'ElasticPress - Last Sync', 'elasticpress' ), - 'fields' => $first_group['fields'], + 'fields' => $first_group['fields'] ?? [], ]; return $debug_info; 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/includes/classes/StatusReport/FailedQueries.php b/includes/classes/StatusReport/FailedQueries.php index f118918592..9206dfb32d 100644 --- a/includes/classes/StatusReport/FailedQueries.php +++ b/includes/classes/StatusReport/FailedQueries.php @@ -137,6 +137,15 @@ protected function maybe_clear_logs() { } $this->query_logger->clear_logs(); + + if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) { + $redirect_url = network_admin_url( 'admin.php?page=elasticpress-status-report' ); + } else { + $redirect_url = admin_url( 'admin.php?page=elasticpress-status-report' ); + } + + wp_safe_redirect( $redirect_url ); + exit(); } /** diff --git a/includes/dashboard.php b/includes/dashboard.php index cf71a7b57c..5751fced34 100644 --- a/includes/dashboard.php +++ b/includes/dashboard.php @@ -771,7 +771,7 @@ function use_language_in_setting( $language = 'english', $context = '' ) { 'czech' => [ 'cs' ], 'danish' => [ 'da' ], 'dutch' => [ 'nl_NL_formal', 'nl_NL', 'nl_BE' ], - 'english' => [ 'en', 'en_AU', 'en_GB', 'en_NZ', 'en_CA', 'en_ZA' ], + 'english' => [ 'en', 'en_AU', 'en_GB', 'en_NZ', 'en_CA', 'en_US', 'en_ZA' ], 'estonian' => [ 'et' ], 'finnish' => [ 'fi' ], 'french' => [ 'fr', 'fr_CA', 'fr_FR', 'fr_BE' ], @@ -852,6 +852,10 @@ function use_language_in_setting( $language = 'english', $context = '' ) { return "_{$language}_"; } + if ( 'filter_ep_stop' === $context ) { + return "_{$language}_"; + } + return $language; } diff --git a/includes/mappings/comment/7-0.php b/includes/mappings/comment/7-0.php index ea6f6105b1..9dfe859c60 100644 --- a/includes/mappings/comment/7-0.php +++ b/includes/mappings/comment/7-0.php @@ -64,15 +64,8 @@ 'analyzer' => [ 'default' => [ 'tokenizer' => 'standard', - 'filter' => [ 'ewp_word_delimiter', 'lowercase', 'stop', 'ewp_snowball' ], - /** - * Filter Elasticsearch default language in mapping - * - * @hook ep_analyzer_language - * @param {string} $lang Default language - * @param {string} $lang_context Language context - * @return {string} New language - */ + 'filter' => [ 'ewp_word_delimiter', 'lowercase', 'ep_stop', 'ewp_snowball' ], + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'analyzer_default' ), ], 'shingle_analyzer' => [ @@ -98,14 +91,7 @@ ], 'ewp_snowball' => [ 'type' => 'snowball', - /** - * Filter Elasticsearch default language in mapping - * - * @hook ep_analyzer_language - * @param {string} $lang Default language - * @param {string} $lang_context Language context - * @return {string} New language - */ + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ewp_snowball' ), ], 'edge_ngram' => [ @@ -114,6 +100,12 @@ 'min_gram' => 3, 'type' => 'edge_ngram', ], + 'ep_stop' => [ + 'type' => 'stop', + 'ignore_case' => true, + /* This filter is documented in includes/mappings/post/7-0.php */ + 'stopwords' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ep_stop' ), + ], ], 'normalizer' => [ 'lowerasciinormalizer' => [ diff --git a/includes/mappings/comment/initial.php b/includes/mappings/comment/initial.php index 4888a635af..90cfe957b8 100644 --- a/includes/mappings/comment/initial.php +++ b/includes/mappings/comment/initial.php @@ -48,15 +48,8 @@ 'analyzer' => [ 'default' => [ 'tokenizer' => 'standard', - 'filter' => [ 'standard', 'ewp_word_delimiter', 'lowercase', 'stop', 'ewp_snowball' ], - /** - * Filter Elasticsearch default language in mapping - * - * @hook ep_analyzer_language - * @param {string} $lang Default language - * @param {string} $lang_context Language context - * @return {string} New language - */ + 'filter' => [ 'standard', 'ewp_word_delimiter', 'lowercase', 'ep_stop', 'ewp_snowball' ], + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'analyzer_default' ), ], 'shingle_analyzer' => [ @@ -82,14 +75,7 @@ ], 'ewp_snowball' => [ 'type' => 'snowball', - /** - * Filter Elasticsearch default language in mapping - * - * @hook ep_analyzer_language - * @param {string} $lang Default language - * @param {string} $lang_context Language context - * @return {string} New language - */ + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ewp_snowball' ), ], 'edge_ngram' => [ @@ -98,6 +84,12 @@ 'min_gram' => 3, 'type' => 'edgeNGram', ], + 'ep_stop' => [ + 'type' => 'stop', + 'ignore_case' => true, + /* This filter is documented in includes/mappings/post/7-0.php */ + 'stopwords' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ep_stop' ), + ], ], 'normalizer' => [ 'lowerasciinormalizer' => [ diff --git a/includes/mappings/post/5-2.php b/includes/mappings/post/5-2.php index fd3086bbbb..fae2c0f86f 100644 --- a/includes/mappings/post/5-2.php +++ b/includes/mappings/post/5-2.php @@ -56,7 +56,7 @@ * @param {array} $filters Default filters * @return {array} New filters */ - 'filter' => apply_filters( 'ep_default_analyzer_filters', array( 'standard', 'ewp_word_delimiter', 'lowercase', 'stop', 'ewp_snowball' ) ), + 'filter' => apply_filters( 'ep_default_analyzer_filters', array( 'standard', 'ewp_word_delimiter', 'lowercase', 'ep_stop', 'ewp_snowball' ) ), /** * Filter Elasticsearch default analyzer's char_filter * @@ -66,14 +66,7 @@ * @return {array} New filters */ 'char_filter' => apply_filters( 'ep_default_analyzer_char_filters', array( 'html_strip' ) ), - /** - * Filter Elasticsearch default language in mapping - * - * @hook ep_analyzer_language - * @param {string} $lang Default language - * @param {string} $lang_context Language context - * @return {string} New language - */ + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'analyzer_default' ), ), 'shingle_analyzer' => array( @@ -99,14 +92,7 @@ ), 'ewp_snowball' => array( 'type' => 'snowball', - /** - * Filter Elasticsearch default language in mapping - * - * @hook ep_analyzer_language - * @param {string} $lang Default language - * @param {string} $lang_context Language context - * @return {string} New language - */ + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ewp_snowball' ), ), 'edge_ngram' => array( @@ -115,6 +101,12 @@ 'min_gram' => 3, 'type' => 'edgeNGram', ), + 'ep_stop' => [ + 'type' => 'stop', + 'ignore_case' => true, + /* This filter is documented in includes/mappings/post/7-0.php */ + 'stopwords' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ep_stop' ), + ], ), 'normalizer' => array( 'lowerasciinormalizer' => array( diff --git a/includes/mappings/post/7-0.php b/includes/mappings/post/7-0.php index 9041c5ee8f..1abbc07b7e 100644 --- a/includes/mappings/post/7-0.php +++ b/includes/mappings/post/7-0.php @@ -115,7 +115,7 @@ ), 'ewp_snowball' => array( 'type' => 'snowball', - /* This filter is documented above */ + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ewp_snowball' ), ), 'edge_ngram' => array( @@ -124,12 +124,12 @@ 'min_gram' => 3, 'type' => 'edge_ngram', ), - 'ep_stop' => array( + 'ep_stop' => [ 'type' => 'stop', 'ignore_case' => true, - /* This filter is documented above */ + /* This filter is documented in includes/mappings/post/7-0.php */ 'stopwords' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ep_stop' ), - ), + ], ), 'normalizer' => array( 'lowerasciinormalizer' => array( diff --git a/includes/mappings/term/7-0.php b/includes/mappings/term/7-0.php index a9cb0e3a0c..96579dde52 100644 --- a/includes/mappings/term/7-0.php +++ b/includes/mappings/term/7-0.php @@ -28,7 +28,8 @@ 'analyzer' => [ 'default' => [ 'tokenizer' => 'standard', - 'filter' => [ 'ewp_word_delimiter', 'lowercase', 'stop', 'ewp_snowball' ], + 'filter' => [ 'ewp_word_delimiter', 'lowercase', 'ep_stop', 'ewp_snowball' ], + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'analyzer_default' ), ], 'shingle_analyzer' => [ @@ -54,6 +55,7 @@ ], 'ewp_snowball' => [ 'type' => 'snowball', + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ewp_snowball' ), ], 'edge_ngram' => [ @@ -62,6 +64,12 @@ 'min_gram' => 3, 'type' => 'edge_ngram', ], + 'ep_stop' => [ + 'type' => 'stop', + 'ignore_case' => true, + /* This filter is documented in includes/mappings/post/7-0.php */ + 'stopwords' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ep_stop' ), + ], ], 'normalizer' => [ 'lowerasciinormalizer' => [ diff --git a/includes/mappings/term/initial.php b/includes/mappings/term/initial.php index d1772ee40d..dcdfc0a163 100644 --- a/includes/mappings/term/initial.php +++ b/includes/mappings/term/initial.php @@ -18,7 +18,7 @@ 'analyzer' => [ 'default' => [ 'tokenizer' => 'standard', - 'filter' => [ 'standard', 'ewp_word_delimiter', 'lowercase', 'stop', 'ewp_snowball' ], + 'filter' => [ 'standard', 'ewp_word_delimiter', 'lowercase', 'ep_stop', 'ewp_snowball' ], 'language' => apply_filters( 'ep_analyzer_language', 'english', 'analyzer_default' ), ], 'shingle_analyzer' => [ @@ -44,6 +44,7 @@ ], 'ewp_snowball' => [ 'type' => 'snowball', + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ewp_snowball' ), ], 'edge_ngram' => [ @@ -52,6 +53,12 @@ 'min_gram' => 3, 'type' => 'edgeNGram', ], + 'ep_stop' => [ + 'type' => 'stop', + 'ignore_case' => true, + /* This filter is documented in includes/mappings/post/7-0.php */ + 'stopwords' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ep_stop' ), + ], ], 'normalizer' => [ 'lowerasciinormalizer' => [ diff --git a/includes/mappings/user/7-0.php b/includes/mappings/user/7-0.php index 20a3b6f49b..cf4301fc6b 100644 --- a/includes/mappings/user/7-0.php +++ b/includes/mappings/user/7-0.php @@ -64,15 +64,8 @@ 'analyzer' => array( 'default' => array( 'tokenizer' => 'standard', - 'filter' => array( 'ewp_word_delimiter', 'lowercase', 'stop', 'ewp_snowball' ), - /** - * Filter Elasticsearch default language in mapping - * - * @hook ep_analyzer_language - * @param {string} $lang Default language - * @param {string} $lang_context Language context - * @return {string} New language - */ + 'filter' => array( 'ewp_word_delimiter', 'lowercase', 'ep_stop', 'ewp_snowball' ), + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'analyzer_default' ), ), 'shingle_analyzer' => array( @@ -98,14 +91,7 @@ ), 'ewp_snowball' => array( 'type' => 'snowball', - /** - * Filter Elasticsearch default language in mapping - * - * @hook ep_analyzer_language - * @param {string} $lang Default language - * @param {string} $lang_context Language context - * @return {string} New language - */ + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ewp_snowball' ), ), 'edge_ngram' => array( @@ -114,6 +100,12 @@ 'min_gram' => 3, 'type' => 'edge_ngram', ), + 'ep_stop' => [ + 'type' => 'stop', + 'ignore_case' => true, + /* This filter is documented in includes/mappings/post/7-0.php */ + 'stopwords' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ep_stop' ), + ], ), 'normalizer' => array( 'lowerasciinormalizer' => array( diff --git a/includes/mappings/user/initial.php b/includes/mappings/user/initial.php index 063a671f39..76dc598468 100644 --- a/includes/mappings/user/initial.php +++ b/includes/mappings/user/initial.php @@ -48,15 +48,8 @@ 'analyzer' => array( 'default' => array( 'tokenizer' => 'standard', - 'filter' => array( 'standard', 'ewp_word_delimiter', 'lowercase', 'stop', 'ewp_snowball' ), - /** - * Filter Elasticsearch default language in mapping - * - * @hook ep_analyzer_language - * @param {string} $lang Default language - * @param {string} $lang_context Language context - * @return {string} New language - */ + 'filter' => array( 'standard', 'ewp_word_delimiter', 'lowercase', 'ep_stop', 'ewp_snowball' ), + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'analyzer_default' ), ), 'shingle_analyzer' => array( @@ -82,14 +75,7 @@ ), 'ewp_snowball' => array( 'type' => 'snowball', - /** - * Filter Elasticsearch default language in mapping - * - * @hook ep_analyzer_language - * @param {string} $lang Default language - * @param {string} $lang_context Language context - * @return {string} New language - */ + /* This filter is documented in includes/mappings/post/7-0.php */ 'language' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ewp_snowball' ), ), 'edge_ngram' => array( @@ -98,6 +84,12 @@ 'min_gram' => 3, 'type' => 'edgeNGram', ), + 'ep_stop' => [ + 'type' => 'stop', + 'ignore_case' => true, + /* This filter is documented in includes/mappings/post/7-0.php */ + 'stopwords' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ep_stop' ), + ], ), 'normalizer' => array( 'lowerasciinormalizer' => array( diff --git a/lang/elasticpress.pot b/lang/elasticpress.pot index 7efd6cdc80..3f301a8cf5 100644 --- a/lang/elasticpress.pot +++ b/lang/elasticpress.pot @@ -2,14 +2,14 @@ # This file is distributed under the GPL v2 or later. msgid "" msgstr "" -"Project-Id-Version: ElasticPress 4.6.0\n" +"Project-Id-Version: ElasticPress 4.6.1\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/elasticpress\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2023-06-13T13:37:04+00:00\n" +"POT-Creation-Date: 2023-07-05T16:24:55+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.7.1\n" "X-Domain: elasticpress\n" @@ -585,46 +585,50 @@ msgstr "" msgid "Recommend alternative search terms for misspelled queries or terms with no results." msgstr "" +#: includes/classes/Feature/DidYouMean/DidYouMean.php:28 +msgid "https://elasticpress.zendesk.com/hc/en-us/articles/16673223107085-Did-You-Mean" +msgstr "" + #. translators: Tutorial URL -#: includes/classes/Feature/DidYouMean/DidYouMean.php:64 +#: includes/classes/Feature/DidYouMean/DidYouMean.php:66 msgid "\"Did You Mean\" search feature provides alternative suggestions for misspelled or ambiguous search queries, enhancing search accuracy and user experience. To display suggestions in your theme, please follow this tutorial." msgstr "" -#: includes/classes/Feature/DidYouMean/DidYouMean.php:142 +#: includes/classes/Feature/DidYouMean/DidYouMean.php:144 msgid "Did you mean" msgstr "" -#: includes/classes/Feature/DidYouMean/DidYouMean.php:227 +#: includes/classes/Feature/DidYouMean/DidYouMean.php:229 #: includes/classes/Feature/Search/Synonyms.php:89 #: includes/classes/Feature/SearchOrdering/SearchOrdering.php:186 msgid "This feature requires the \"Post Search\" feature to be enabled" msgstr "" -#: includes/classes/Feature/DidYouMean/DidYouMean.php:242 +#: includes/classes/Feature/DidYouMean/DidYouMean.php:244 msgid "Search behavior when no result is found" msgstr "" -#: includes/classes/Feature/DidYouMean/DidYouMean.php:244 +#: includes/classes/Feature/DidYouMean/DidYouMean.php:246 msgid "Display the top suggestion" msgstr "" -#: includes/classes/Feature/DidYouMean/DidYouMean.php:245 +#: includes/classes/Feature/DidYouMean/DidYouMean.php:247 msgid "Display all the suggestions" msgstr "" -#: includes/classes/Feature/DidYouMean/DidYouMean.php:246 +#: includes/classes/Feature/DidYouMean/DidYouMean.php:248 msgid "Automatically redirect the user to the top suggestion" msgstr "" -#: includes/classes/Feature/DidYouMean/DidYouMean.php:284 +#: includes/classes/Feature/DidYouMean/DidYouMean.php:286 msgid "Other suggestions:" msgstr "" -#: includes/classes/Feature/DidYouMean/DidYouMean.php:384 +#: includes/classes/Feature/DidYouMean/DidYouMean.php:386 msgid "Showing results for: " msgstr "" -#: includes/classes/Feature/DidYouMean/DidYouMean.php:386 +#: includes/classes/Feature/DidYouMean/DidYouMean.php:388 msgid "No results for: " msgstr "" @@ -699,7 +703,7 @@ msgid "\"All\" will only show content that matches all facets. \"Any\" will show msgstr "" #. translators: URL -#: includes/classes/Feature/Facets/Facets.php:502 +#: includes/classes/Feature/Facets/Facets.php:505 msgid "Adds a Facet widget that administrators can add to the website's sidebars (widgetized areas), so that visitors can filter applicable content and search results by one or more taxonomy terms." msgstr "" diff --git a/package-lock.json b/package-lock.json index 9f7af18fc5..68998ce0ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "elasticpress", - "version": "4.6.0", + "version": "4.6.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "elasticpress", - "version": "4.6.0", + "version": "4.6.1", "license": "GPL-2.0-or-later", "dependencies": { "@10up/component-tooltip": "^2.0.0", diff --git a/package.json b/package.json index 67672da575..059e2f5778 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "elasticpress", - "version": "4.6.0", + "version": "4.6.1", "license": "GPL-2.0-or-later", "description": "A fast and flexible search and query engine for WordPress.", "devDependencies": { diff --git a/readme.txt b/readme.txt index 0cc59bf530..e75eccd80a 100644 --- a/readme.txt +++ b/readme.txt @@ -2,7 +2,7 @@ Contributors: 10up, tlovett1, vhauri, tott, oscarssanchez, cmmarslender Tags: performance, slow, search, elasticsearch, fuzzy, facet, aggregation, searching, autosuggest, suggest, elastic, advanced search, woocommerce, related posts, woocommerce Tested up to: 6.2 -Stable tag: 4.6.0 +Stable tag: 4.6.1 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -81,6 +81,30 @@ For sure! Feel free to submit ideas or feedback in general to our [GitHub repo]( == Changelog == += 4.6.1 - 2023-07-05 = + +**Note that starting from the ElasticPress 5.0.0 release the `Users` feature will be moved to the [ElasticPress Labs](https://github.com/10up/ElasticPressLabs) plugin. The `Terms` and `Comments` features will remain in ElasticPress but will be available only if enabled via code. Check [our blog post](https://www.elasticpress.io/blog/2023/03/enabling-comments-and-terms-in-elasticpress-5-0) for more info.** + +__Added:__ + +* Add doc url for "Did You Mean" feature. Props [@burhandodhy](https://github.com/burhandodhy). + +__Changed:__ + +* Use `wp_cache_supports` over `wp_cache_supports_group_flush`. Props [@spacedmonkey](https://github.com/spacedmonkey). +* Update the `ep_exclude_from_search` post meta only if it is set or has some value. Props [@MARQAS](https://github.com/MARQAS) and [@columbian-chris](https://github.com/columbian-chris). + +__Fixed:__ + +* Deprecation notice in `ElasticPress\Feature\WooCommerce\Orders`. Props [@mwidmann](https://github.com/mwidmann). +* Don't apply a facet filter to the query if the filter value is empty. Props [@burhandodhy](https://github.com/burhandodhy). +* Syncing a post with empty post meta key. Props [@MARQAS](https://github.com/MARQAS) and [@oscarssanchez](https://github.com/oscarssanchez). +* Order by clauses with Elasticsearch field formats are not changed anymore. Props [@felipeelia](https://github.com/felipeelia) and [@tlovett1](https://github.com/tlovett1). +* Failed Query logs are automatically cleared on refreshing the "Status Report" page. Props [@burhandodhy](https://github.com/burhandodhy). +* Warning message on Health page when Last Sync information is not available. Props [@burhandodhy](https://github.com/burhandodhy). +* Deprecation notice: json_encode(): Passing null to parameter #2. Props [@burhandodhy](https://github.com/burhandodhy). +* Documentation of the `ep_facet_search_get_terms_args` filter. Props [@burhandodhy](https://github.com/burhandodhy). + = 4.6.0 - 2023-06-13 = **Note that starting from the ElasticPress 5.0.0 release the `Users` feature will be moved to the [ElasticPress Labs](https://github.com/10up/ElasticPressLabs) plugin. The `Terms` and `Comments` features will remain in ElasticPress but will be available only if enabled via code. Check [our blog post](https://www.elasticpress.io/blog/2023/03/enabling-comments-and-terms-in-elasticpress-5-0) for more info.** diff --git a/tests/php/TestCommands.php b/tests/php/TestCommands.php index 721577a940..ede446e1f7 100644 --- a/tests/php/TestCommands.php +++ b/tests/php/TestCommands.php @@ -1032,6 +1032,31 @@ public function testThrowsErrorWhenHostIsNotSet() { $this->command->sync( [], [] ); } + /** + * Test get-index-settings command returns an index settings. + * + * @since 4.7.0 + */ + public function testGetIndexSettings() { + $this->command->get_index_settings( [ 'exampleorg-post-1' ], [] ); + + $output = $this->getActualOutputForAssertion(); + $this->assertStringStartsWith( '{', $output ); + $this->assertStringContainsString( 'index.mapping.total_fields.limit', $output ); + + // clean output buffer + ob_clean(); + + // test with --pretty flag + $this->command->get_index_settings( [ 'exampleorg-post-1' ], [ 'pretty' => true ] ); + + $output = $this->getActualOutputForAssertion(); + $this->assertStringStartsWith( "{\n", $output ); + + // clean output buffer + ob_clean(); + } + /** * Test commands throws an error if indexing is already happening. */ diff --git a/tests/php/TestDashboard.php b/tests/php/TestDashboard.php new file mode 100644 index 0000000000..b9b697e765 --- /dev/null +++ b/tests/php/TestDashboard.php @@ -0,0 +1,93 @@ +assertSame( 'english', Dashboard\use_language_in_setting() ); + + $existing_lang = function () { + return 'ar'; + }; + add_filter( 'ep_default_language', $existing_lang ); + $this->assertSame( 'arabic', Dashboard\use_language_in_setting() ); + + $existing_lang = function () { + return 'non-existent'; + }; + add_filter( 'ep_default_language', $existing_lang ); + $this->assertSame( 'english', Dashboard\use_language_in_setting() ); + } + + /** + * Test the default behavior of the `use_language_in_setting` function for the `filter_ewp_snowball` context + * + * @group dashboard + */ + public function test_use_language_in_setting_for_snowball() { + $this->assertSame( 'English', Dashboard\use_language_in_setting( '', 'filter_ewp_snowball' ) ); + + $existing_lang = function () { + return 'hy'; + }; + add_filter( 'ep_default_language', $existing_lang ); + $this->assertSame( 'Armenian', Dashboard\use_language_in_setting( '', 'filter_ewp_snowball' ) ); + + $existing_lang = function () { + return 'non-existent'; + }; + add_filter( 'ep_default_language', $existing_lang ); + $this->assertSame( 'English', Dashboard\use_language_in_setting( '', 'filter_ewp_snowball' ) ); + } + + /** + * Test the default behavior of the `use_language_in_setting` function for the `filter_ewp_snowball` context + * + * @group dashboard + */ + public function test_use_language_in_setting_for_stop() { + $this->assertSame( '_english_', Dashboard\use_language_in_setting( '', 'filter_ep_stop' ) ); + + $existing_lang = function () { + return 'ar'; + }; + add_filter( 'ep_default_language', $existing_lang ); + $this->assertSame( '_arabic_', Dashboard\use_language_in_setting( '', 'filter_ep_stop' ) ); + + $existing_lang = function () { + return 'non-existent'; + }; + add_filter( 'ep_default_language', $existing_lang ); + $this->assertSame( '_english_', Dashboard\use_language_in_setting( '', 'filter_ep_stop' ) ); + } +} diff --git a/tests/php/TestUninstall.php b/tests/php/TestUninstall.php new file mode 100644 index 0000000000..86258962cf --- /dev/null +++ b/tests/php/TestUninstall.php @@ -0,0 +1,68 @@ +uninstaller = new \EP_Uninstaller(); + + parent::set_up(); + } + + /** + * Test the `delete_transients_by_option_name` method + * + * @group uninstall + */ + public function test_delete_transients_by_option_name() { + set_transient( 'ep_total_fields_limit_test', 'test' ); + set_transient( 'ep_total_fields_limit_test_2', 'test' ); + set_transient( 'ep_related_posts_test', 'test' ); + set_transient( 'ep_related_posts_test_2', 'test' ); + + $method = $this->get_protected_method( 'delete_transients_by_option_name' ); + $method->invoke( $this->uninstaller ); + + $this->assertFalse( get_transient( 'ep_total_fields_limit_test' ) ); + $this->assertFalse( get_transient( 'ep_total_fields_limit_test_2' ) ); + $this->assertFalse( get_transient( 'ep_related_posts_test' ) ); + $this->assertFalse( get_transient( 'ep_related_posts_test_2' ) ); + } + + /** + * Return a protected method made public. + * + * This should NOT be copied to any other class. + * + * @param string $method_name The method name + * @return \ReflectionMethod + */ + protected function get_protected_method( string $method_name ) : \ReflectionMethod { + $reflection = new \ReflectionClass( '\EP_Uninstaller' ); + $method = $reflection->getMethod( $method_name ); + $method->setAccessible( true ); + + return $method; + } +} diff --git a/tests/php/features/TestDocuments.php b/tests/php/features/TestDocuments.php index c068ab8fa5..c010d3fc0d 100644 --- a/tests/php/features/TestDocuments.php +++ b/tests/php/features/TestDocuments.php @@ -187,4 +187,43 @@ public function testSearchNormalPost() { $this->assertTrue( $query->elasticsearch_success ); $this->assertEquals( 1, count( $query->posts ) ); } + + /** + * Tests query doesn't return the post if `ep_exclude_from_search` meta is set. + * + * @since 4.7.0 + */ + public function testExcludeFromSearchQuery() { + ElasticPress\Features::factory()->activate_feature( 'search' ); + ElasticPress\Features::factory()->activate_feature( 'documents' ); + ElasticPress\Features::factory()->setup_features(); + + $this->ep_factory->post->create_many( + 2, + array( + 'post_content' => 'find me in search', + 'meta_input' => array( 'ep_exclude_from_search' => false ), + 'post_type' => 'attachment', + 'post_mime_type' => 'application/msword', + ) + ); + $this->ep_factory->post->create( + array( + 'post_content' => 'exclude from search', + 'meta_input' => array( 'ep_exclude_from_search' => true ), + 'post_type' => 'attachment', + 'post_mime_type' => 'application/msword', + ) + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $args = array( + 's' => 'search', + ); + $query = new \WP_Query( $args ); + + $this->assertTrue( $query->elasticsearch_success ); + $this->assertEquals( 2, $query->post_count ); + } } diff --git a/tests/php/features/TestFacet.php b/tests/php/features/TestFacet.php index bcf9551424..381ca387a7 100644 --- a/tests/php/features/TestFacet.php +++ b/tests/php/features/TestFacet.php @@ -105,6 +105,12 @@ public function testGetSelected() { $this->assertSelectedTax( array( $term->slug => true ), 'taxonomy', $selected ); $this->assertArrayHasKey( 'post_type', $selected ); $this->assertSame( 'posttype', $selected['post_type'] ); + + // test when filter value is empty. + parse_str( 'ep_filter_category=&ep_filter_othertax=amet&s=', $_GET ); + $selected = $facet_feature->get_selected(); + $this->assertArrayNotHasKey( 'category', $selected['taxonomies'] ); + $this->assertArrayHasKey( 's', $selected ); } /** 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..826dd8eeb7 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' ); @@ -80,20 +63,15 @@ public function testProductsPostTypeQueryOn() { } /** - * Test products post type query does get integrated when querying WC product_cat taxonomy + * Test products post type query does not get automatically integrated when querying WC product_cat taxonomy * - * @since 2.1 * @group woocommerce + * @group woocommerce-products */ public function testProductsPostTypeQueryProductCatTax() { - ElasticPress\Features::factory()->activate_feature( 'admin' ); ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); ElasticPress\Features::factory()->setup_features(); - $this->ep_factory->post->create(); - - ElasticPress\Elasticsearch::factory()->refresh_indices(); - $args = array( 'tax_query' => array( array( @@ -106,210 +84,81 @@ 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', - ) - ); + $this->assertNull( $query->elasticsearch_success ); - ElasticPress\Elasticsearch::factory()->refresh_indices(); + $args = [ 'product_cat' => 'cat' ]; - $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. + * Test WC product_cat taxonomy queries do get automatically integrated when ep_integrate is set to true * - * @since 4.5 + * @since 4.7.0 + * @group woocommerce + * @group woocommerce-products */ - public function testShopOrderPostTypeQueryWhenProtectedContentEnable() { - ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + public function testProductsPostTypeQueryProductCatTaxWhenEPIntegrateSetTrue() { 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 = [ + 'product_cat' => 'cat', + 'ep_integrate' => true, + ]; - $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. + * Test WC product_cat taxonomy queries do get automatically integrated for the main query * - * @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 + * @since 4.7.0 * @group woocommerce + * @group woocommerce-products */ - public function testSearchShopOrderById() { - ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + public function testProductsPostTypeQueryProductCatTaxWhenMainQuery() { + global $wp_the_query; + 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 ); + $wp_the_query->query( $args ); - $this->assertTrue( $query->elasticsearch_success ); - $this->assertEquals( 1, $query->post_count ); - $this->assertEquals( 1, $query->found_posts ); + $this->assertTrue( $wp_the_query->elasticsearch_success ); } /** - * 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. + * Test products post type query does get automatically integrated for the main query * - * @since 4.0.0 + * @since 4.7.0 * @group woocommerce + * @group woocommerce-products */ - public function testSearchShopOrderByMetaFieldAndId() { - ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + public function testProductsPostTypeQueryProductWhenMainQuery() { + global $wp_the_query; + 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 ); + $wp_the_query->query( [ 'post_type' => 'product' ] ); - $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 ); + $this->assertTrue( $wp_the_query->elasticsearch_success ); } /** * 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 +176,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 +317,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 +325,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 +396,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 +413,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 +465,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 +518,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 +588,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 +648,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 +705,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 +729,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 +753,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 +773,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(); - add_filter( 'ep_woocommerce_orders_autosuggest_available', '__return_false' ); - $this->assertFalse( $woocommerce_feature->is_orders_autosuggest_available() ); + $variation_2 = new \WC_Product_Variation(); + $variation_2->set_parent_id( $main_product_id ); + $variation_2->set_sku( 'child-sku-2' ); + $variation_2->save(); + + $main_product_as_post = get_post( $main_product_id ); + $product_meta_to_index = ElasticPress\Features::factory() + ->get_registered_feature( 'woocommerce' ) + ->products + ->add_variations_skus_meta( [], $main_product_as_post ); + + $this->assertArrayHasKey( '_variations_skus', $product_meta_to_index ); + $this->assertContains( 'child-sku-1', $product_meta_to_index['_variations_skus'] ); + $this->assertContains( 'child-sku-2', $product_meta_to_index['_variations_skus'] ); } /** - * 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 +921,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 +962,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' ); + } } diff --git a/tests/php/indexables/TestComment.php b/tests/php/indexables/TestComment.php index 815b6bfd22..dbfa2fa7ef 100644 --- a/tests/php/indexables/TestComment.php +++ b/tests/php/indexables/TestComment.php @@ -2457,4 +2457,32 @@ public function testCommentIndexingWithProtectedContentEnabled() { $this->assertCount( 2, $comments ); } + + /** + * Test if the mapping applies the ep_stop filter correctly + * + * @since 4.7.0 + * @group comments + */ + public function test_mapping_ep_stop_filter() { + $indexable = ElasticPress\Indexables::factory()->get( 'comment' ); + $index_name = $indexable->get_index_name(); + $settings = ElasticPress\Elasticsearch::factory()->get_index_settings( $index_name ); + $index_settings = $settings[ $index_name ]['settings']; + + $this->assertContains( 'ep_stop', $index_settings['index.analysis.analyzer.default.filter'] ); + $this->assertSame( '_english_', $index_settings['index.analysis.filter.ep_stop.stopwords'] ); + + $change_lang = function( $lang, $context ) { + return 'filter_ep_stop' === $context ? '_arabic_' : $lang; + }; + add_filter( 'ep_analyzer_language', $change_lang, 11, 2 ); + + ElasticPress\Elasticsearch::factory()->delete_all_indices(); + $indexable->put_mapping(); + + $settings = ElasticPress\Elasticsearch::factory()->get_index_settings( $index_name ); + $index_settings = $settings[ $index_name ]['settings']; + $this->assertSame( '_arabic_', $index_settings['index.analysis.filter.ep_stop.stopwords'] ); + } } diff --git a/tests/php/indexables/TestPost.php b/tests/php/indexables/TestPost.php index dabc9c83f6..ca9e65c7a9 100644 --- a/tests/php/indexables/TestPost.php +++ b/tests/php/indexables/TestPost.php @@ -3886,6 +3886,45 @@ public function filter_ep_prepare_meta_excluded_public_keys( $meta_keys ) { } + /** + * Test to verify that empty meta key should be excluded before sync. + * + * @since 4.6.1 + * @group post + */ + public function testEmptyMetaKey() { + global $wpdb; + $post_id = $this->ep_factory->post->create(); + $post = get_post( $post_id ); + $meta_key = ''; + $meta_value_1 = 'Meta value for empty key'; + $meta_values = array( + 'value 1', + 'value 2', + ); + add_post_meta( $post_id, 'test_meta_1', $meta_values ); + + $wpdb->insert( + $wpdb->postmeta, + array( + 'post_id' => $post_id, + 'meta_key' => $meta_key, + 'meta_value' => $meta_value_1, + ), + array( '%d', '%s', '%s' ) + ); + $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->postmeta WHERE meta_key=%s AND post_id = %d", $meta_key, $post_id ) ); + + $this->assertSame( $meta_key, $row->meta_key ); + $this->assertSame( $meta_value_1, $row->meta_value ); + + $meta_data = ElasticPress\Indexables::factory()->get( 'post' )->prepare_meta( $post ); + + $this->assertIsArray( $meta_data ); + $this->assertCount( 1, $meta_data ); + $this->assertArrayHasKey( 'test_meta_1', $meta_data ); + } + /** * Test meta preparation * @@ -6867,6 +6906,55 @@ public function testParseOrderbyMetaQueryTypes( $meta_value_type, $es_type ) { $this->assertGreaterThanOrEqual( 1, did_filter( 'ep_formatted_args' ) ); } + /** + * Test the parse_orderby_meta_fields() method when dealing with multiple meta fields + * + * @see https://github.com/10up/ElasticPress/issues/3509 + * @since 4.6.1 + * @group post + */ + public function testParseOrderbyMetaMultiple() { + $method_executed = false; + + $query_args = [ + 'ep_integrate' => true, + 'orderby' => [ + 'meta_field1' => 'desc', + 'meta.meta_field3.double' => 'asc', + 'meta.meta_field2.double' => 'asc', + ], + 'meta_query' => [ + 'date_clause' => [ + 'key' => 'meta_field2', + 'value' => '20230622', + 'compare' => '>=', + ], + ], + ]; + + $assert_callback = function( $args ) use ( &$method_executed ) { + $method_executed = true; + + $expected_sort = [ + [ 'meta_field1' => [ 'order' => 'desc' ] ], + [ 'meta.meta_field3.double' => [ 'order' => 'asc' ] ], + [ 'meta.meta_field2.double' => [ 'order' => 'asc' ] ], + ]; + + $this->assertSame( $expected_sort, $args['sort'] ); + + return $args; + }; + + // Run the tests. + add_filter( 'ep_formatted_args', $assert_callback ); + $query = new \WP_Query( $query_args ); + remove_filter( 'ep_formatted_args', $assert_callback ); + + $this->assertTrue( $method_executed ); + $this->assertGreaterThanOrEqual( 1, did_filter( 'ep_formatted_args' ) ); + } + /** * Tests additional nested tax queries in parse_tax_query(). * @@ -8023,6 +8111,30 @@ public function testExcludeFromSearchQuery() { $this->assertEquals( 2, $query->post_count ); } + /** + * Tests that post meta value should be empty when it is not set. + * + * @since 4.6.1 + * @group post + */ + public function testMetaValueNotSet() { + $post_ids = array(); + $post_ids[0] = $this->ep_factory->post->create( + array( + 'post_content' => 'find me in search', + ) + ); + $post_ids[1] = $this->ep_factory->post->create( + array( + 'post_content' => 'exlcude from search', + 'meta_input' => array( 'ep_exclude_from_search' => true ), + ) + ); + + $this->assertEmpty( get_post_meta( $post_ids[0], 'ep_exclude_from_search', true ) ); + $this->assertEquals( 1, get_post_meta( $post_ids[1], 'ep_exclude_from_search', true ) ); + } + /** * Tests search term is wrapped in html tag with custom class */ @@ -8878,4 +8990,32 @@ public function testKillSyncForPasswordProtected() { add_filter( 'ep_pre_kill_sync_for_password_protected', $dont_kill_pw_post, 10, 3 ); $this->assertFalse( $sync_manager->kill_sync_for_password_protected( false, $pw_post ) ); } + + /** + * Test if the mapping applies the ep_stop filter correctly + * + * @since 4.7.0 + * @group post + */ + public function test_mapping_ep_stop_filter() { + $indexable = ElasticPress\Indexables::factory()->get( 'post' ); + $index_name = $indexable->get_index_name(); + $settings = ElasticPress\Elasticsearch::factory()->get_index_settings( $index_name ); + $index_settings = $settings[ $index_name ]['settings']; + + $this->assertContains( 'ep_stop', $index_settings['index.analysis.analyzer.default.filter'] ); + $this->assertSame( '_english_', $index_settings['index.analysis.filter.ep_stop.stopwords'] ); + + $change_lang = function( $lang, $context ) { + return 'filter_ep_stop' === $context ? '_arabic_' : $lang; + }; + add_filter( 'ep_analyzer_language', $change_lang, 11, 2 ); + + ElasticPress\Elasticsearch::factory()->delete_all_indices(); + $indexable->put_mapping(); + + $settings = ElasticPress\Elasticsearch::factory()->get_index_settings( $index_name ); + $index_settings = $settings[ $index_name ]['settings']; + $this->assertSame( '_arabic_', $index_settings['index.analysis.filter.ep_stop.stopwords'] ); + } } diff --git a/tests/php/indexables/TestTerm.php b/tests/php/indexables/TestTerm.php index ab5f9a8981..6696d558c6 100644 --- a/tests/php/indexables/TestTerm.php +++ b/tests/php/indexables/TestTerm.php @@ -1722,4 +1722,32 @@ public function testQueryForNonPublicTaxonomies() { $this->assertArrayNotHasKey( 'elasticsearch_success', $properties ); $this->assertEquals( 4, count( $term_query->terms ) ); } + + /** + * Test if the mapping applies the ep_stop filter correctly + * + * @since 4.7.0 + * @group term + */ + public function test_mapping_ep_stop_filter() { + $indexable = ElasticPress\Indexables::factory()->get( 'term' ); + $index_name = $indexable->get_index_name(); + $settings = ElasticPress\Elasticsearch::factory()->get_index_settings( $index_name ); + $index_settings = $settings[ $index_name ]['settings']; + + $this->assertContains( 'ep_stop', $index_settings['index.analysis.analyzer.default.filter'] ); + $this->assertSame( '_english_', $index_settings['index.analysis.filter.ep_stop.stopwords'] ); + + $change_lang = function( $lang, $context ) { + return 'filter_ep_stop' === $context ? '_arabic_' : $lang; + }; + add_filter( 'ep_analyzer_language', $change_lang, 11, 2 ); + + ElasticPress\Elasticsearch::factory()->delete_all_indices(); + $indexable->put_mapping(); + + $settings = ElasticPress\Elasticsearch::factory()->get_index_settings( $index_name ); + $index_settings = $settings[ $index_name ]['settings']; + $this->assertSame( '_arabic_', $index_settings['index.analysis.filter.ep_stop.stopwords'] ); + } } diff --git a/tests/php/indexables/TestUser.php b/tests/php/indexables/TestUser.php index 99cf28dde3..88353f5c6b 100644 --- a/tests/php/indexables/TestUser.php +++ b/tests/php/indexables/TestUser.php @@ -1599,4 +1599,31 @@ public function testQueryDb() { $this->assertEquals( $user_1, $results['objects'][0]->ID ); } + /** + * Test if the mapping applies the ep_stop filter correctly + * + * @since 4.7.0 + * @group user + */ + public function test_mapping_ep_stop_filter() { + $indexable = ElasticPress\Indexables::factory()->get( 'user' ); + $index_name = $indexable->get_index_name(); + $settings = ElasticPress\Elasticsearch::factory()->get_index_settings( $index_name ); + $index_settings = $settings[ $index_name ]['settings']; + + $this->assertContains( 'ep_stop', $index_settings['index.analysis.analyzer.default.filter'] ); + $this->assertSame( '_english_', $index_settings['index.analysis.filter.ep_stop.stopwords'] ); + + $change_lang = function( $lang, $context ) { + return 'filter_ep_stop' === $context ? '_arabic_' : $lang; + }; + add_filter( 'ep_analyzer_language', $change_lang, 11, 2 ); + + ElasticPress\Elasticsearch::factory()->delete_all_indices(); + $indexable->put_mapping(); + + $settings = ElasticPress\Elasticsearch::factory()->get_index_settings( $index_name ); + $index_settings = $settings[ $index_name ]['settings']; + $this->assertSame( '_arabic_', $index_settings['index.analysis.filter.ep_stop.stopwords'] ); + } } diff --git a/uninstall.php b/uninstall.php index 1996985c28..e171a8e931 100644 --- a/uninstall.php +++ b/uninstall.php @@ -81,12 +81,16 @@ class EP_Uninstaller { * @since 1.7 */ public function __construct() { - // Exit if accessed directly. if ( ! defined( 'ABSPATH' ) ) { $this->exit_uninstaller(); } + // If testing, do not do anything automatically + if ( defined( 'EP_UNIT_TESTS' ) && EP_UNIT_TESTS ) { + return; + } + // EP_MANUAL_SETTINGS_RESET is used by the `settings-reset` WP-CLI command. if ( ! defined( 'EP_MANUAL_SETTINGS_RESET' ) || ! EP_MANUAL_SETTINGS_RESET ) { // Not uninstalling. @@ -124,51 +128,41 @@ protected function delete_transients() { } /** - * Delete all transients of the Related Posts feature. + * Delete remaining transients by their option names. + * + * @since 4.7.0 */ - protected function delete_related_posts_transients() { + protected function delete_transients_by_option_name() { global $wpdb; - $related_posts_transients = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery - "SELECT option_name FROM {$wpdb->prefix}options WHERE option_name LIKE '_transient_ep_related_posts_%'" + $transients = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + "SELECT option_name + FROM {$wpdb->prefix}options + WHERE + option_name LIKE '_transient_ep_total_fields_limit_%' + OR option_name LIKE '_transient_ep_related_posts_%' + " ); - foreach ( $related_posts_transients as $related_posts_transient ) { - $related_posts_transient = str_replace( '_transient_', '', $related_posts_transient ); - delete_site_transient( $related_posts_transient ); - delete_transient( $related_posts_transient ); + foreach ( $transients as $transient ) { + $transient_name = str_replace( '_transient_', '', $transient ); + delete_site_transient( $transient_name ); + delete_transient( $transient_name ); } } /** - * DEPRECATED. Delete all transients of the total fields limit. + * DEPRECATED. Delete all transients of the Related Posts feature. */ - protected function delete_total_fields_limit_transients() { - _doing_it_wrong( __METHOD__, 'delete_indices_settings_transients() should be call instead.', 'ElasticPress 4.7.0' ); + protected function delete_related_posts_transients() { + _deprecated_function( __METHOD__, '4.7.0', '\EP_Uninstaller::delete_transients_by_name()' ); } /** - * Delete all transients for cached indices settings. - * - * @since 4.7.0 + * DEPRECATED. Delete all transients of the total fields limit. */ - protected function delete_indices_settings_transients() { - global $wpdb; - - $indices_settings_transients = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery - "SELECT option_name - FROM {$wpdb->prefix}options - WHERE option_name - LIKE '_transient_ep_total_fields_limit_%' - OR LIKE '_transient_ep_default_analyzer_language_%' - OR LIKE '_transient_ep_snowball_filter_language_%'" - ); - - foreach ( $indices_settings_transients as $indices_settings_transient ) { - $indices_settings_transient = str_replace( '_transient_', '', $indices_settings_transient ); - delete_site_transient( $indices_settings_transient ); - delete_transient( $indices_settings_transient ); - } + protected function delete_total_fields_limit_transients() { + _deprecated_function( __METHOD__, '4.7.0', '\EP_Uninstaller::delete_transients_by_name()' ); } /** @@ -194,16 +188,14 @@ protected function clean_options_and_transients() { $this->delete_options(); $this->delete_transients(); - $this->delete_related_posts_transients(); - $this->delete_indices_settings_transients(); + $this->delete_transients_by_option_name(); restore_current_blog(); } } else { $this->delete_options(); $this->delete_transients(); - $this->delete_related_posts_transients(); - $this->delete_indices_settings_transients(); + $this->delete_transients_by_option_name(); } }