slug = 'instant-results'; $this->group = 'live-search'; $this->host = trailingslashit( Utils\get_host() ); $this->is_woocommerce = function_exists( 'WC' ); $this->default_settings = [ 'highlight_tag' => 'mark', 'facets' => 'post_type,tax-category,tax-post_tag', 'match_type' => 'all', 'term_count' => '1', 'numbered_pagination' => '0', 'per_page' => get_option( 'posts_per_page', 6 ), 'search_behavior' => '0', ]; $this->settings = $this->get_settings(); $this->requires_install_reindex = true; $this->available_during_installation = true; $this->is_powered_by_epio = Utils\is_epio(); parent::__construct(); } /** * Sets i18n strings. * * @return void * @since 5.2.0 */ public function set_i18n_strings(): void { $this->title = esc_html__( 'Instant Results', 'elasticpress' ); $this->short_title = esc_html__( 'Instant Results', 'elasticpress' ); $this->summary = '
' . __( 'WordPress search forms will display results instantly. When the search query is submitted, a modal will open that populates results by querying ElasticPress directly, bypassing WordPress. As the user refines their search, results are refreshed.', 'elasticpress' ) . '
' . '' . __( 'Requires an ElasticPress.io plan or a custom proxy to function.', 'elasticpress' ) . '
'; $this->docs_url = __( 'https://www.elasticpress.io/resources/articles/configuring-elasticpress-via-the-plugin-dashboard/#instant-results', 'elasticpress' ); } /** * Tell user whether requirements for feature are met or not. * * @return array $status Status array */ public function requirements_status() { $status = new FeatureRequirementsStatus( 2, null, $this ); $status->message = []; if ( Utils\is_epio() ) { $status->code = 1; /** * Whether the feature is available for non ElasticPress.io customers. * * Installations using self-hosted Elasticsearch will need to implement an API for * handling search requests before making the feature available. * * @since 4.0.0 * @hook ep_instant_results_available * @param {string} $available Whether the feature is available. */ } elseif ( apply_filters( 'ep_instant_results_available', false ) ) { $status->code = 1; $status->message[] = esc_html__( 'You are using a custom proxy. Make sure you implement all security measures needed.', 'elasticpress' ); } else { $status->message[] = wp_kses_post( __( 'To use this feature you need to be an ElasticPress.io customer or implement a custom proxy.', 'elasticpress' ) ); } /** * Display a warning if ElasticPress is network activated. */ if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) { $status->message[] = wp_kses_post( sprintf( /* translators: Article URL */ __( 'ElasticPress is network activated. Additional steps are required to ensure Instant Results works for all sites on the network. See our article on running ElasticPress in network mode for more details.', 'elasticpress' ), 'https://www.elasticpress.io/resources/articles/running-elasticpress-in-a-wordpress-multisite-network-mode/' ) ); } return $status; } /** * Setup feature functionality. * * @return void */ public function setup() { add_filter( 'ep_after_update_feature', [ $this, 'after_update_feature' ], 10, 3 ); add_filter( 'ep_formatted_args', [ $this, 'maybe_apply_aggs_args' ], 10, 3 ); add_filter( 'ep_post_mapping', [ $this, 'add_mapping_properties' ] ); add_filter( 'ep_post_sync_args', [ $this, 'add_post_sync_args' ], 10, 2 ); add_action( 'ep_after_sync_index', [ $this, 'on_sync_complete' ] ); add_filter( 'ep_saved_weighting_configuration', [ $this, 'epio_save_search_template' ] ); add_action( 'pre_get_posts', [ $this, 'maybe_apply_product_visibility' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_assets' ] ); add_action( 'wp_footer', [ $this, 'render' ] ); } /** * Output modal markup. */ public function render() { echo ''; } /** * Enqueue our autosuggest script. */ public function enqueue_frontend_assets() { if ( Utils\is_indexing() ) { return; } wp_enqueue_style( 'elasticpress-instant-results', EP_URL . 'dist/css/instant-results-styles.css', Utils\get_asset_info( 'instant-results-styles', 'dependencies' ), Utils\get_asset_info( 'instant-results-styles', 'version' ) ); wp_enqueue_script( 'elasticpress-instant-results', EP_URL . 'dist/js/instant-results-script.js', Utils\get_asset_info( 'instant-results-script', 'dependencies' ), Utils\get_asset_info( 'instant-results-script', 'version' ), true ); wp_set_script_translations( 'elasticpress-instant-results', 'elasticpress' ); $index = Indexables::factory()->get( 'post' )->get_index_name(); /** * The search API endpoint. * * @since 4.0.0 * @hook ep_instant_results_search_endpoint * @param {string} $endpoint Endpoint path. * @param {string} $index Elasticsearch index. */ $api_endpoint = apply_filters( 'ep_instant_results_search_endpoint', "api/v1/search/posts/{$index}", $index ); wp_localize_script( 'elasticpress-instant-results', 'epInstantResults', array( 'apiEndpoint' => $api_endpoint, 'apiHost' => ( 0 !== strpos( $api_endpoint, 'http' ) ) ? esc_url_raw( $this->host ) : '', 'argsSchema' => $this->get_args_schema(), 'currencyCode' => $this->is_woocommerce ? get_woocommerce_currency() : false, 'excludedPostTypes' => $this->get_excluded_post_types(), 'excludedTermIds' => $this->get_excluded_term_ids(), 'facets' => $this->get_facets_for_frontend(), 'highlightTag' => $this->settings['highlight_tag'], 'isWooCommerce' => $this->is_woocommerce, 'locale' => str_replace( '_', '-', get_locale() ), 'matchType' => $this->settings['match_type'], 'paramPrefix' => 'ep-', 'postTypeLabels' => $this->get_post_type_labels(), 'termCount' => $this->settings['term_count'], 'numberedPagination' => $this->settings['numbered_pagination'], 'requestIdBase' => Utils\get_request_id_base(), 'showSuggestions' => \ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' )->is_active(), 'suggestionsBehavior' => $this->settings['search_behavior'], ) ); } /** * Get the endpoint for the Instant Results search template. * * @return string Instant Results search template endpoint. */ public function get_template_endpoint(): string { $index = Indexables::factory()->get( 'post' )->get_index_name(); /** * Filters the search template API endpoint. * * @since 4.0.0 * @hook ep_instant_results_template_endpoint * @param {string} $endpoint Endpoint path. * @param {string} $index Elasticsearch index. * @returns {string} Search template API endpoint. */ return apply_filters( 'ep_instant_results_template_endpoint', "api/v1/search/posts/{$index}/template/", $index ); } /** * 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(): string { $post_types = Features::factory()->get_registered_feature( 'search' )->get_searchable_post_types(); $excluded_post_types = $this->get_excluded_post_types(); $post_types = array_values( array_diff( $post_types, $excluded_post_types ) ); $post_statuses = get_post_stati( [ 'public' => true, 'exclude_from_search' => false, ] ); /** * The ID of the current user when generating the Instant Results * search template. * * By default Instant Results sets the current user as anomnymous when * generating the search template, so that any filters applied to * queries for logged-in or specific users are not applied to the * template. This filter supports setting a specific user as the * current user while the template is generated. * * @since 4.1.0 * @hook ep_search_template_user_id * @param {int} $user_id User ID to use. * @return {int} New user ID to use. */ $template_user_id = apply_filters( 'ep_search_template_user_id', 0 ); $original_user_id = get_current_user_id(); wp_set_current_user( $template_user_id ); 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_search_template' => true, 'post_status' => array_values( $post_statuses ), 'post_type' => $post_types, 's' => '{{ep_placeholder}}', 'ep_intercept_request' => true, 'ep_skip_search_exclusion' => true, ) ); remove_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10 ); remove_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10 ); wp_set_current_user( $original_user_id ); return $this->search_template; } /** * Return true if a given feature is supported by Instant Results. * * 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 instant * search. */ public function is_integrated_request( $is_integrated, $context ) { $supported_contexts = [ 'autosuggest', 'documents', 'search', 'weighting', '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 ); } /** * If generating the search template query, do not bypass the post exclusion * * @since 4.4.0 * @param bool $bypass_exclusion_from_search Whether the post exclusion from search should be applied or not * @param WP_Query $query The WP Query * @return bool */ public function maybe_bypass_post_exclusion( $bypass_exclusion_from_search, $query ) { _doing_it_wrong( __METHOD__, esc_html__( 'Use the WP_Query argument `ep_skip_search_exclusion`.', 'elasticpress' ), 'ElasticPress 5.3.0' ); return true === $query->get( 'ep_search_template' ) ? false : // not bypass, apply $bypass_exclusion_from_search; } /** * Apply product visibility taxonomy query to search template queries. * * @param \WP_Query $query Query instance. * @return void */ public function maybe_apply_product_visibility( $query ) { if ( true !== $query->get( 'ep_search_template' ) ) { return; } if ( ! $this->is_woocommerce ) { return; } $this->apply_product_visibility( $query ); } /** * Apply product visibility taxonomy query. * * Applies filters to exclude products set to be excluded from search. Out * of stock products will also be excluded if WooCommerce is configured to * hide those products. * * Mimics the logic of WC_Query::get_tax_query(). * * @param \WP_Query $query Query instance. * @return void */ public function apply_product_visibility( $query ) { $product_visibility_terms = wc_get_product_visibility_term_ids(); $product_visibility_not_in = (array) $product_visibility_terms['exclude-from-search']; if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { $product_visibility_not_in[] = $product_visibility_terms['outofstock']; } if ( ! empty( $product_visibility_not_in ) ) { $tax_query = $query->get( 'tax_query', array() ); $tax_query[] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => $product_visibility_not_in, 'operator' => 'NOT IN', ); $query->set( 'tax_query', $tax_query ); } } /** * Apply aggregation args to search templates. * * @param array $formatted_args Formatted Elasticsearch query. * @param array $query_vars Query variables * @param \WP_Query $query Query instance. * @return array Formatted Elasticsearch query. */ public function maybe_apply_aggs_args( $formatted_args, $query_vars, $query ) { if ( true !== $query->get( 'ep_search_template' ) ) { return $formatted_args; } return $this->apply_aggs_args( $formatted_args ); } /** * Add aggregation args to Elasticsearch query for facets. * * @param array $formatted_args Formatted Elasticsearch query. * @return array Formatted Elasticsearch query. */ public function apply_aggs_args( $formatted_args ) { $filter = $formatted_args['post_filter']; $facets = $this->get_facets(); foreach ( $facets as $key => $facet ) { $formatted_args['aggs'][ $key ]['aggs'] = $facet['aggs']; if ( $filter ) { $formatted_args['aggs'][ $key ]['filter'] = $filter; } } return $formatted_args; } /** * Add additional fields to post mapping. * * @param array $mapping Post mapping. * @return array Post mapping. */ public function add_mapping_properties( $mapping ) { $elasticsearch_version = Elasticsearch::factory()->get_elasticsearch_version(); $properties = array( 'post_content_plain' => array( 'type' => 'text' ), 'price_html' => array( 'type' => 'text' ), ); if ( version_compare( (string) $elasticsearch_version, '7.0', '<' ) ) { $mapping['mappings']['post']['properties'] = array_merge( $mapping['mappings']['post']['properties'], $properties ); } else { $mapping['mappings']['properties'] = array_merge( $mapping['mappings']['properties'], $properties ); } return $mapping; } /** * Add data for additional mapping properties. * * @param array $post_args Post arguments. * @param integer $post_id Post ID. * @return array Post sync args. */ public function add_post_sync_args( $post_args, $post_id ) { $post = get_post( $post_id ); $post_args['post_content_plain'] = $this->prepare_plain_content_arg( $post ); $post_args['price_html'] = $this->prepare_price_html_arg( $post ); return $post_args; } /** * Get data for the plain post content. * * @param WP_Post $post Post object. * @return string Post content. */ public function prepare_plain_content_arg( $post ) { $post_content = apply_filters( 'the_content', $post->post_content ); return wp_strip_all_tags( $post_content ); } /** * Get data for the price HTML arg. * * @param WP_Post $post Post object. * @return string|null Price HTML. */ public function prepare_price_html_arg( $post ) { if ( 'product' !== $post->post_type ) { return null; } if ( ! $this->is_woocommerce ) { return null; } $product = wc_get_product( $post ); return $product->get_price_html(); } /** * Get post type labels. * * Only the post type slug is indexed, so we'll need the labels on the * front end for display. * * @return array Array of post types and their labels. */ public function get_post_type_labels() { $labels = []; $post_types = Features::factory()->get_registered_feature( 'search' )->get_searchable_post_types(); foreach ( $post_types as $post_type ) { $post_type_object = get_post_type_object( $post_type ); $post_type_labels = get_post_type_labels( $post_type_object ); $labels[ $post_type ] = array( 'plural' => $post_type_labels->name, 'singular' => $post_type_labels->singular_name, ); } return $labels; } /** * Get post type slugs to exclude from the Instant Results post type filter. * * @since 5.3.4 * @return string[] Post type slugs to exclude. */ public function get_excluded_post_types(): array { /** * Filter post type slugs to exclude from Instant Results. * * Excluded post types are hidden from the Post Type facet and * removed from the search template so their content does not * appear in results. The search template is generated and * saved when Instant Results settings are saved, * so after changing this filter, re-save the Instant Results * or Weighting settings to regenerate the template. * * @hook ep_instant_results_excluded_post_types * @since 5.3.4 * @param {string[]} $excluded Post type slugs to exclude (e.g., ['page']). * @return {string[]} Filtered exclusions. */ $post_types = apply_filters( 'ep_instant_results_excluded_post_types', [] ); $post_types = array_filter( (array) $post_types, 'is_scalar' ); $post_types = array_map( 'strval', $post_types ); $post_types = array_filter( $post_types ); return array_values( $post_types ); } /** * Get term IDs to exclude from all Instant Results taxonomy filters. * * @since 5.3.4 * @return int[] Term IDs to exclude. */ public function get_excluded_term_ids(): array { /** * Filter term IDs to exclude from all Instant Results taxonomy filters. * * Excluded terms are hidden from every taxonomy facet * (Category, Tag, etc.) but posts associated with those * terms still appear in search results. * * @hook ep_instant_results_excluded_term_ids * @since 5.3.4 * @param {int[]} $excluded Term IDs to exclude (e.g., [1, 120]). * @return {int[]} Filtered exclusions. */ $term_ids = apply_filters( 'ep_instant_results_excluded_term_ids', [] ); $term_ids = array_filter( (array) $term_ids, 'is_scalar' ); $term_ids = array_map( 'intval', $term_ids ); $term_ids = array_filter( $term_ids ); return array_values( $term_ids ); } /** * Get available facets. * * @return array Available facets. */ public function get_facets() { $facets = []; /** * Post type facet. */ $facets['post_type'] = array( 'type' => 'post_type', 'post_types' => [], 'labels' => array( 'admin' => __( 'Post type', 'elasticpress' ), 'frontend' => __( 'Type', 'elasticpress' ), ), 'aggs' => array( 'post_type' => array( 'terms' => array( 'field' => 'post_type.raw', ), ), ), /** * The post_type arg needs to be supported regardless of whether * the Post Type facet is present to be able to support setting the * post type from the search form. * * @see ElasticPress\Feature\InstantResults::get_args_schema() */ 'args' => array(), ); /** * Taxonomy facets. */ $taxonomies = get_taxonomies( array( 'public' => true ), 'object' ); $taxonomies = apply_filters( 'ep_facet_include_taxonomies', $taxonomies ); foreach ( $taxonomies as $slug => $taxonomy ) { if ( is_string( $taxonomy ) ) { $slug = $taxonomy; $taxonomy = get_taxonomy( $slug ); } if ( ! ( $taxonomy instanceof \WP_Taxonomy ) ) { _doing_it_wrong( __METHOD__, sprintf( /* translators: %s is a taxonomy slug. */ esc_html__( 'Invalid taxonomy "%s" returned via ep_facet_include_taxonomies filter', 'elasticpress' ), esc_html( $slug ) ), 'ElasticPress 5.3.3' ); continue; } $name = 'tax-' . $slug; $labels = get_taxonomy_labels( $taxonomy ); $admin_label = sprintf( /* translators: $1$s: Taxonomy name. %2$s: Taxonomy slug. */ esc_html__( '%1$s (%2$s)' ), $labels->singular_name, $slug ); $post_types = Features::factory()->get_registered_feature( 'search' )->get_searchable_post_types(); $post_types = array_intersect( $post_types, $taxonomy->object_type ); $post_types = array_values( $post_types ); $facets[ $name ] = array( 'type' => 'taxonomy', 'post_types' => $post_types, 'labels' => array( 'admin' => wp_specialchars_decode( $admin_label, ENT_QUOTES ), 'frontend' => wp_specialchars_decode( $labels->singular_name, ENT_QUOTES ), ), 'aggs' => array( $name => array( 'terms' => array( 'field' => 'terms.' . $slug . '.facet', 'size' => apply_filters( 'ep_facet_taxonomies_size', 10000, $taxonomy ), ), ), ), 'args' => array( $name => array( 'type' => 'strings', ), ), ); } /** * Price facet. */ if ( $this->is_woocommerce ) { $facets['price_range'] = array( 'type' => 'price_range', 'post_types' => [ 'product' ], 'labels' => array( 'admin' => __( 'Price range', 'elasticpress' ), 'frontend' => __( 'Price', 'elasticpress' ), ), 'aggs' => array( 'max_price' => array( 'max' => array( 'field' => 'meta._price.double', ), ), 'min_price' => array( 'min' => array( 'field' => 'meta._price.double', ), ), ), 'args' => array( 'max_price' => array( 'type' => 'number', ), 'min_price' => array( 'type' => 'number', ), ), ); } return $facets; } /** * Get facet configuration for the front end. * * @return Array Facet configuration for the front end. */ public function get_facets_for_frontend() { $selected_facets = explode( ',', $this->settings['facets'] ); $available_facets = $this->get_facets(); $facets = []; foreach ( $selected_facets as $key ) { if ( isset( $available_facets[ $key ] ) ) { $facet = $available_facets[ $key ]; $facets[] = array( 'name' => $key, 'label' => $facet['labels']['frontend'], 'type' => $facet['type'], 'postTypes' => $facet['post_types'], ); } } return $facets; } /** * Get facet configuration for the admin. * * @return Array Facet configuration for the admin. */ public function get_facets_for_admin() { $available_facets = $this->get_facets(); $facets = []; foreach ( $available_facets as $key => $facet ) { $facets[ $key ] = array( 'label' => $facet['labels']['admin'], 'value' => $key, ); } return $facets; } /** * Get schema for search args. * * @return array Search args schema. */ public function get_args_schema() { /** * The number of results per page for Instant Results. * * @since 4.5.0 * @hook ep_instant_results_per_page * @param {int} $per_page Results per page. */ $per_page = apply_filters( 'ep_instant_results_per_page', $this->settings['per_page'] ); $args_schema = array( 'highlight' => array( 'type' => 'string', 'default' => $this->settings['highlight_tag'], 'allowedValues' => [ $this->settings['highlight_tag'] ], ), 'offset' => array( 'type' => 'number', 'default' => 0, ), 'orderby' => array( 'type' => 'string', 'default' => 'relevance', 'allowedValues' => [ 'date', 'price', 'relevance' ], ), 'order' => array( 'type' => 'string', 'default' => 'desc', 'allowedValues' => [ 'asc', 'desc' ], ), 'per_page' => array( 'type' => 'number', 'default' => absint( $per_page ), ), 'post_type' => array( 'type' => 'strings', ), 'search' => array( 'type' => 'string', 'default' => '', ), 'relation' => array( 'type' => 'string', 'default' => 'all' === $this->settings['match_type'] ? 'and' : 'or', 'allowedValues' => [ 'and', 'or' ], ), ); $selected_facets = explode( ',', $this->settings['facets'] ); $available_facets = $this->get_facets(); foreach ( $selected_facets as $key ) { if ( isset( $available_facets[ $key ] ) ) { $args_schema = array_merge( $args_schema, $available_facets[ $key ]['args'] ); } } /** * The schema defining the API arguments used by Instant Results. * * The argument schema is used to configure the APISearchProvider * component used by Instant Results, and should conform to what is * supported by the API being used. The Instant Results UI expects * the default list of arguments to be available, so caution is advised * when adding or removing arguments. * * @since 4.5.1 * @hook ep_instant_results_args_schema * @param {array} $args_schema Results per page. */ return apply_filters( 'ep_instant_results_args_schema', $args_schema ); } /** * Set the `settings_schema` attribute * * @since 5.0.0 */ protected function set_settings_schema() { $facets = $this->get_facets_for_admin(); $this->settings_schema = [ [ 'default' => 'mark', 'help' => __( 'Select the HTML tag used to highlight search terms.', 'elasticpress' ), 'key' => 'highlight_tag', 'label' => __( 'Highlight tag', 'elasticpress' ), 'options' => [ [ 'label' => __( 'None', 'elasticpress' ), 'value' => '', ], [ 'label' => 'mark', 'value' => 'mark', ], [ 'label' => 'span', 'value' => 'span', ], [ 'label' => 'strong', 'value' => 'strong', ], [ 'label' => 'em', 'value' => 'em', ], [ 'label' => 'i', 'value' => 'i', ], ], 'type' => 'select', ], [ 'default' => 'post_type,tax-category,tax-post_tag', 'key' => 'facets', 'label' => __( 'Filters', 'elasticpress' ), 'options' => array_values( $facets ), 'type' => 'multiple', ], [ 'default' => 'all', 'key' => 'match_type', 'label' => __( 'Filter matching', 'elasticpress' ), 'options' => [ [ 'label' => __( 'Show results that match all selected filters', 'elasticpress' ), 'value' => 'all', ], [ 'label' => __( 'Show results that match any selected filter', 'elasticpress' ), 'value' => 'any', ], ], 'type' => 'radio', ], [ 'default' => '1', 'help' => __( 'Enable to show the number of matching results next to filter options.', 'elasticpress' ), 'key' => 'term_count', 'label' => __( 'Show filter counts', 'elasticpress' ), 'type' => 'checkbox', ], [ 'default' => '0', 'help' => __( 'Enable to show numbered pagination links instead of previous/next buttons.', 'elasticpress' ), 'key' => 'numbered_pagination', 'label' => __( 'Numbered pagination', 'elasticpress' ), 'type' => 'checkbox', ], [ 'default' => get_option( 'posts_per_page', 6 ), 'key' => 'per_page', 'type' => 'hidden', ], [ 'default' => '0', 'key' => 'search_behavior', 'label' => __( 'Search behavior when no result is found', 'elasticpress' ), 'options' => [ [ 'label' => __( 'Display the top suggestion', 'elasticpress' ), 'value' => '0', ], [ 'label' => __( 'Display all the suggestions', 'elasticpress' ), 'value' => 'list', ], ], 'requires_feature' => 'did-you-mean', 'type' => 'radio', ], ]; } /** * Callback for ep_after_sync_index to save search templates. * * @param array $args Sync arguments containing network_wide flag. * @return void * @since 5.3.3 */ public function on_sync_complete( array $args ): void { $network_wide = isset( $args['network_wide'] ) && ! is_null( $args['network_wide'] ); $this->epio_save_site_search_template( $network_wide ); } /** * Save the search template for the current site or all network sites. * * @param bool $network_wide Whether to save templates for all sites in the network. * @return void * @since 5.3.3 */ public function epio_save_site_search_template( bool $network_wide = false ): void { if ( ! $network_wide ) { $this->epio_save_search_template(); return; } $sites = Utils\get_sites( 0, true ); foreach ( $sites as $site ) { switch_to_blog( $site['blog_id'] ); $this->epio_save_search_template(); restore_current_blog(); } } /** * Delete the search template for the current site or all network sites. * * @param bool $network_wide Whether to delete templates for all sites in the network. * @return void * @since 5.3.3 */ public function epio_delete_site_search_template( bool $network_wide = false ): void { if ( ! $network_wide ) { $this->epio_delete_search_template(); return; } $sites = Utils\get_sites( 0, true ); foreach ( $sites as $site ) { switch_to_blog( $site['blog_id'] ); $this->epio_delete_search_template(); restore_current_blog(); } } }