← All changes
|
jetpack_vendor/automattic/jetpack-search/src/class-helper.php
+385
-35
12.0.3
→
16.3
View file →
| @@ -10,8 +10,12 @@ | ||
| 10 | 10 | use Automattic\Jetpack\Status; |
| 11 | 11 | use GP_Locales; |
| 12 | 12 | use Jetpack; // TODO: Remove this once migrated. |
| 13 | 13 | |
| 14 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 15 | + exit( 0 ); | |
| 16 | +} | |
| 17 | + | |
| 14 | 18 | /** |
| 15 | 19 | * Various helper functions for reuse throughout the Jetpack Search code. |
| 16 | 20 | */ |
| 17 | 21 | class Helper { |
| @@ -176,12 +180,17 @@ | ||
| 176 | 180 | if ( empty( $widget_filter['name'] ) ) { |
| 177 | 181 | $widget_filter['name'] = self::generate_widget_filter_name( $widget_filter ); |
| 178 | 182 | } |
| 179 | 183 | |
| 180 | - $type = ( isset( $widget_filter['type'] ) ) ? $widget_filter['type'] : ''; | |
| 181 | - $key = sprintf( '%s_%d', $type, count( $filters ) ); | |
| 184 | + $type = $widget_filter['type'] ?? ''; | |
| 182 | 185 | |
| 183 | - $filters[ $key ] = $widget_filter; | |
| 186 | + // If this is a product_attribute filter with no specific attribute, expand it to all global attributes. | |
| 187 | + if ( 'product_attribute' === $type && empty( $widget_filter['attribute'] ) ) { | |
| 188 | + $filters = self::expand_product_attribute_filters( $widget_filter, $filters ); | |
| 189 | + } else { | |
| 190 | + $key = sprintf( '%s_%d', $type, count( $filters ) ); | |
| 191 | + $filters[ $key ] = $widget_filter; | |
| 192 | + } | |
| 184 | 193 | } |
| 185 | 194 | } |
| 186 | 195 | |
| 187 | 196 | return $filters; |
| @@ -187,8 +196,47 @@ | ||
| 187 | 196 | return $filters; |
| 188 | 197 | } |
| 189 | 198 | |
| 190 | 199 | /** |
| 200 | + * Expands a product_attribute filter into individual filters for each attribute. | |
| 201 | + * | |
| 202 | + * @since 5.8.0 | |
| 203 | + * | |
| 204 | + * @param array $widget_filter The filter configuration. | |
| 205 | + * @param array $filters The existing filters array. | |
| 206 | + * @return array The filters array with expanded product attribute filters. | |
| 207 | + */ | |
| 208 | + private static function expand_product_attribute_filters( $widget_filter, $filters ) { | |
| 209 | + if ( ! function_exists( 'wc_get_attribute_taxonomies' ) || ! function_exists( 'wc_attribute_taxonomy_name' ) ) { | |
| 210 | + return $filters; | |
| 211 | + } | |
| 212 | + | |
| 213 | + $product_attributes = wc_get_attribute_taxonomies(); | |
| 214 | + $included_attributes = isset( $widget_filter['included_attributes'] ) ? (array) $widget_filter['included_attributes'] : array(); | |
| 215 | + | |
| 216 | + // If no attributes are explicitly included, show all attributes (backward compatibility). | |
| 217 | + // Also optimize by treating "all selected" the same as "none selected" to avoid O(n²) in_array() checks. | |
| 218 | + $show_all = empty( $included_attributes ) || count( $included_attributes ) === count( $product_attributes ); | |
| 219 | + | |
| 220 | + foreach ( $product_attributes as $attribute ) { | |
| 221 | + $attribute_name = wc_attribute_taxonomy_name( $attribute->attribute_name ); | |
| 222 | + | |
| 223 | + if ( ! $show_all && ! in_array( $attribute_name, $included_attributes, true ) ) { | |
| 224 | + continue; | |
| 225 | + } | |
| 226 | + | |
| 227 | + $key = sprintf( 'product_attribute_%d', count( $filters ) ); | |
| 228 | + $expanded_filter = $widget_filter; | |
| 229 | + $expanded_filter['attribute'] = $attribute_name; | |
| 230 | + $expanded_filter['name'] = $attribute->attribute_label; | |
| 231 | + unset( $expanded_filter['included_attributes'] ); | |
| 232 | + $filters[ $key ] = $expanded_filter; | |
| 233 | + } | |
| 234 | + | |
| 235 | + return $filters; | |
| 236 | + } | |
| 237 | + | |
| 238 | + /** | |
| 191 | 239 | * Get the localized default label for a date filter. |
| 192 | 240 | * |
| 193 | 241 | * @since 5.8.0 |
| 194 | 242 | * |
| @@ -277,8 +325,13 @@ | ||
| 277 | 325 | } elseif ( isset( $tax->labels ) && isset( $tax->labels->name ) ) { |
| 278 | 326 | $name = $tax->labels->name; |
| 279 | 327 | } |
| 280 | 328 | break; |
| 329 | + | |
| 330 | + case 'product_attribute': | |
| 331 | + $name = _x( 'Product Attributes', 'label for filtering posts', 'jetpack-search-pkg' ); | |
| 332 | + break; | |
| 333 | + | |
| 281 | 334 | } |
| 282 | 335 | |
| 283 | 336 | return $name; |
| 284 | 337 | } |
| @@ -363,9 +416,9 @@ | ||
| 363 | 416 | $post_types_from_query = array(); |
| 364 | 417 | } elseif ( is_array( $_GET['post_type'] ) ) { |
| 365 | 418 | $post_types_from_query = $_GET['post_type']; |
| 366 | 419 | } else { |
| 367 | - $post_types_from_query = (array) explode( ',', $_GET['post_type'] ); | |
| 420 | + $post_types_from_query = explode( ',', $_GET['post_type'] ); | |
| 368 | 421 | } |
| 369 | 422 | // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput |
| 370 | 423 | |
| 371 | 424 | $post_types_from_query = array_map( 'sanitize_key', $post_types_from_query ); |
| @@ -437,8 +490,12 @@ | ||
| 437 | 490 | break; |
| 438 | 491 | } |
| 439 | 492 | |
| 440 | 493 | if ( 'filters' === $k ) { |
| 494 | + if ( ! is_countable( $new_instance['filters'] ) || ! is_countable( $old_instance['filters'] ) ) { | |
| 495 | + continue; | |
| 496 | + } | |
| 497 | + | |
| 441 | 498 | if ( count( $new_instance['filters'] ) !== count( $old_instance['filters'] ) ) { |
| 442 | 499 | $widget = $new_instance; |
| 443 | 500 | break; |
| 444 | 501 | } |
| @@ -454,9 +511,9 @@ | ||
| 454 | 511 | } |
| 455 | 512 | } |
| 456 | 513 | } |
| 457 | 514 | |
| 458 | - if ( empty( $action ) || empty( $widget ) ) { | |
| 515 | + if ( empty( $widget ) ) { | |
| 459 | 516 | return false; |
| 460 | 517 | } |
| 461 | 518 | |
| 462 | 519 | return array( |
| @@ -681,8 +738,28 @@ | ||
| 681 | 738 | return Options::site_has_vip_index() ? 1000 : 100; |
| 682 | 739 | } |
| 683 | 740 | |
| 684 | 741 | /** |
| 742 | + * Resolves the effective results-per-page value for the Search 3.0 blocks. | |
| 743 | + * | |
| 744 | + * `$override` is the author-set `resultsPerPage` block attribute; `0` (or | |
| 745 | + * any non-positive value) means "use the site's `posts_per_page` Reading | |
| 746 | + * setting." A non-positive Reading setting (e.g. `-1`, "show all") falls | |
| 747 | + * back to 10 rather than clamping down to a single result. Either way the | |
| 748 | + * result is capped at `get_max_posts_per_page()`. | |
| 749 | + * | |
| 750 | + * @param int $override Author override, or 0 to use the site default. | |
| 751 | + * @return int Clamped, always-positive results-per-page value. | |
| 752 | + */ | |
| 753 | + public static function resolve_results_per_page( $override = 0 ) { | |
| 754 | + $value = $override > 0 ? (int) $override : (int) get_option( 'posts_per_page' ); | |
| 755 | + if ( $value <= 0 ) { | |
| 756 | + $value = 10; | |
| 757 | + } | |
| 758 | + return min( $value, self::get_max_posts_per_page() ); | |
| 759 | + } | |
| 760 | + | |
| 761 | + /** | |
| 685 | 762 | * Returns the maximum offset for a search query. |
| 686 | 763 | * |
| 687 | 764 | * @since 5.8.0 |
| 688 | 765 | * |
| @@ -749,9 +826,9 @@ | ||
| 749 | 826 | * Sanitizes a checkbox value for writing to the database. |
| 750 | 827 | * |
| 751 | 828 | * @since 8.9.0 |
| 752 | 829 | * |
| 753 | - * @param any $value from the customizer form. | |
| 830 | + * @param mixed $value from the customizer form. | |
| 754 | 831 | * @return string either '0' or '1'. |
| 755 | 832 | */ |
| 756 | 833 | public static function sanitize_checkbox_value( $value ) { |
| 757 | 834 | return true === $value ? '1' : '0'; |
| @@ -761,9 +838,9 @@ | ||
| 761 | 838 | * Sanitizes a checkbox value for rendering the Customizer. |
| 762 | 839 | * |
| 763 | 840 | * @since 8.9.0 |
| 764 | 841 | * |
| 765 | - * @param any $value from the database. | |
| 842 | + * @param mixed $value from the database. | |
| 766 | 843 | * @return boolean |
| 767 | 844 | */ |
| 768 | 845 | public static function sanitize_checkbox_value_for_js( $value ) { |
| 769 | 846 | return '1' === $value; |
| @@ -835,9 +912,9 @@ | ||
| 835 | 912 | ) |
| 836 | 913 | ); |
| 837 | 914 | $unexcluded_post_types = array_diff( $post_types, $excluded_post_types ); |
| 838 | 915 | // NOTE: If all post types are being excluded, ignore the option value. |
| 839 | - if ( count( $unexcluded_post_types ) === 0 ) { | |
| 916 | + if ( array() === $unexcluded_post_types ) { | |
| 840 | 917 | $excluded_post_types = array(); |
| 841 | 918 | } |
| 842 | 919 | |
| 843 | 920 | $is_wpcom = static::is_wpcom(); |
| @@ -844,44 +921,79 @@ | ||
| 844 | 921 | $is_private_site = ( new Status() )->is_private_site(); |
| 845 | 922 | $is_jetpack_photon_enabled = method_exists( 'Jetpack', 'is_module_active' ) && Jetpack::is_module_active( 'photon' ); |
| 846 | 923 | |
| 847 | 924 | $options = array( |
| 848 | - 'overlayOptions' => array( | |
| 849 | - 'colorTheme' => get_option( $prefix . 'color_theme', 'light' ), | |
| 850 | - 'enableInfScroll' => get_option( $prefix . 'inf_scroll', '1' ) === '1', | |
| 851 | - 'enablePostDate' => get_option( $prefix . 'show_post_date', '1' ) === '1', | |
| 852 | - 'enableSort' => get_option( $prefix . 'enable_sort', '1' ) === '1', | |
| 853 | - 'highlightColor' => get_option( $prefix . 'highlight_color', '#FFC' ), | |
| 854 | - 'overlayTrigger' => get_option( $prefix . 'overlay_trigger', Options::DEFAULT_OVERLAY_TRIGGER ), | |
| 855 | - 'resultFormat' => get_option( $prefix . 'result_format', Options::RESULT_FORMAT_MINIMAL ), | |
| 856 | - 'showPoweredBy' => ( new Plan() )->is_free_plan() || ( get_option( $prefix . 'show_powered_by', '1' ) === '1' ), | |
| 925 | + 'overlayOptions' => array( | |
| 926 | + 'colorTheme' => get_option( $prefix . 'color_theme', 'light' ), | |
| 927 | + 'enableInfScroll' => get_option( $prefix . 'inf_scroll', '1' ) === '1', | |
| 928 | + 'enableFilteringOpensOverlay' => get_option( $prefix . 'filtering_opens_overlay', '1' ) === '1', | |
| 929 | + 'enablePostDate' => get_option( $prefix . 'show_post_date', '1' ) === '1', | |
| 930 | + 'enableProductPrice' => get_option( $prefix . 'show_product_price', '1' ) === '1', | |
| 931 | + 'enableSort' => get_option( $prefix . 'enable_sort', '1' ) === '1', | |
| 932 | + 'highlightColor' => get_option( $prefix . 'highlight_color', '#FFC' ), | |
| 933 | + 'overlayTrigger' => get_option( $prefix . 'overlay_trigger', Options::DEFAULT_OVERLAY_TRIGGER ), | |
| 934 | + 'resultFormat' => get_option( $prefix . 'result_format', Options::RESULT_FORMAT_MINIMAL ), | |
| 935 | + 'showPoweredBy' => ( new Plan() )->is_free_plan() || ( get_option( $prefix . 'show_powered_by', '1' ) === '1' ), | |
| 857 | 936 | |
| 858 | 937 | // These options require kicking off a new search. |
| 859 | - 'defaultSort' => get_option( $prefix . 'default_sort', 'relevance' ), | |
| 860 | - 'excludedPostTypes' => $excluded_post_types, | |
| 938 | + 'defaultSort' => get_option( $prefix . 'default_sort', 'relevance' ), | |
| 939 | + 'excludedPostTypes' => $excluded_post_types, | |
| 940 | + | |
| 941 | + // Fallback image options | |
| 942 | + 'fallbackImageUrl' => get_option( $prefix . 'fallback_image_url', '' ), | |
| 943 | + 'enableFallbackImage' => get_option( $prefix . 'enable_fallback_image', '0' ) === '1', | |
| 861 | 944 | ), |
| 862 | 945 | |
| 863 | 946 | // core config. |
| 864 | - 'homeUrl' => home_url(), | |
| 865 | - 'locale' => str_replace( '_', '-', self::is_valid_locale( get_locale() ) ? get_locale() : 'en_US' ), | |
| 866 | - 'postsPerPage' => $posts_per_page, | |
| 867 | - 'siteId' => self::get_wpcom_site_id(), | |
| 868 | - 'postTypes' => $post_type_labels, | |
| 869 | - 'webpackPublicPath' => plugins_url( '/build/instant-search/', __DIR__ ), | |
| 870 | - 'isPhotonEnabled' => ( $is_wpcom || $is_jetpack_photon_enabled ) && ! $is_private_site, | |
| 871 | - 'isFreePlan' => ( new Plan() )->is_free_plan(), | |
| 947 | + 'homeUrl' => home_url(), | |
| 948 | + 'locale' => str_replace( '_', '-', self::is_valid_locale( get_locale() ) ? get_locale() : 'en_US' ), | |
| 949 | + 'postsPerPage' => $posts_per_page, | |
| 950 | + 'siteId' => self::get_wpcom_site_id(), | |
| 951 | + 'searchSuggestionsEnabled' => (bool) get_option( 'jetpack_search_suggestions_enabled', false ), | |
| 952 | + 'postTypes' => $post_type_labels, | |
| 953 | + 'webpackPublicPath' => plugins_url( '/build/instant-search/', __DIR__ ), | |
| 954 | + 'isPhotonEnabled' => ( $is_wpcom || $is_jetpack_photon_enabled ) && ! $is_private_site, | |
| 955 | + 'isFreePlan' => ( new Plan() )->is_free_plan(), | |
| 872 | 956 | |
| 873 | 957 | // config values related to private site support. |
| 874 | - 'apiRoot' => esc_url_raw( rest_url() ), | |
| 875 | - 'apiNonce' => wp_create_nonce( 'wp_rest' ), | |
| 876 | - 'isPrivateSite' => $is_private_site, | |
| 877 | - 'isWpcom' => $is_wpcom, | |
| 958 | + 'apiRoot' => esc_url_raw( rest_url() ), | |
| 959 | + 'apiNonce' => wp_create_nonce( 'wp_rest' ), | |
| 960 | + 'isPrivateSite' => $is_private_site, | |
| 961 | + 'isWpcom' => $is_wpcom, | |
| 878 | 962 | |
| 879 | 963 | // widget info. |
| 880 | - 'hasOverlayWidgets' => count( $overlay_widget_ids ) > 0, | |
| 881 | - 'widgets' => array_values( $widgets ), | |
| 882 | - 'widgetsOutsideOverlay' => array_values( $widgets_outside_overlay ), | |
| 883 | - 'hasNonSearchWidgets' => $has_non_search_widgets, | |
| 964 | + 'hasOverlayWidgets' => is_countable( $overlay_widget_ids ) && count( $overlay_widget_ids ) > 0, | |
| 965 | + 'widgets' => array_values( $widgets ), | |
| 966 | + 'widgetsOutsideOverlay' => array_values( $widgets_outside_overlay ), | |
| 967 | + 'hasNonSearchWidgets' => $has_non_search_widgets, | |
| 968 | + /** | |
| 969 | + * Whether to prevent tracking cookie reset. More information `pbmxuV-39H-p2`. | |
| 970 | + * | |
| 971 | + * @module search | |
| 972 | + * | |
| 973 | + * @since 0.41.0 | |
| 974 | + * | |
| 975 | + * @param bool Prevent cookie reset for automattic sites as default value. | |
| 976 | + */ | |
| 977 | + 'preventTrackingCookiesReset' => apply_filters( 'jetpack_instant_search_prevent_tracking_cookies_reset', function_exists( 'is_automattic' ) && is_automattic() ), | |
| 978 | + | |
| 979 | + /** | |
| 980 | + * Whether to disable Tracks and TrainTracks analytics. | |
| 981 | + * | |
| 982 | + * This can be enabled via URL parameter (?disable_tracking=1) for testing, | |
| 983 | + * or via the filter for permanent configuration. Useful for debugging issues | |
| 984 | + * where tracking may interfere with search functionality, such as Safari's | |
| 985 | + * advanced tracking protection. | |
| 986 | + * | |
| 987 | + * @module search | |
| 988 | + * | |
| 989 | + * @since 0.56.0 | |
| 990 | + * | |
| 991 | + * @param bool $disable_tracking Whether to disable tracking. Default false. | |
| 992 | + */ | |
| 993 | + 'disableTracking' => self::is_tracking_disabled() || apply_filters( 'jetpack_instant_search_disable_tracking', false ), | |
| 994 | + 'aiAnswersEnabled' => AI_Answers::is_enabled(), | |
| 995 | + 'aiMasterEnabled' => AI_Answers::is_master_enabled(), | |
| 884 | 996 | ); |
| 885 | 997 | |
| 886 | 998 | /** |
| 887 | 999 | * Customize Instant Search Options. |
| @@ -895,8 +1007,224 @@ | ||
| 895 | 1007 | return apply_filters( 'jetpack_instant_search_options', $options ); |
| 896 | 1008 | } |
| 897 | 1009 | |
| 898 | 1010 | /** |
| 1011 | + * Default highlight fields when `highlightFields` is unset in | |
| 1012 | + * `jetpack_instant_search_options`. Matches instant search's JS default. | |
| 1013 | + * | |
| 1014 | + * @since 7.4.0 | |
| 1015 | + */ | |
| 1016 | + const DEFAULT_INSTANT_SEARCH_HIGHLIGHT_FIELDS = array( 'title', 'content', 'comments' ); | |
| 1017 | + | |
| 1018 | + /** | |
| 1019 | + * Fields added to API `fields` when searching additional blogs. | |
| 1020 | + * | |
| 1021 | + * @since 7.4.0 | |
| 1022 | + */ | |
| 1023 | + const MULTISITE_SEARCH_FIELD_NAMES = array( 'author', 'blog_name', 'blog_icon_url', 'blog_id' ); | |
| 1024 | + | |
| 1025 | + /** | |
| 1026 | + * Read Instant Search query-customization options from a filtered options array. | |
| 1027 | + * | |
| 1028 | + * @since 7.4.0 | |
| 1029 | + * | |
| 1030 | + * @param array $options Raw `jetpack_instant_search_options` value. | |
| 1031 | + * @return array Query options with keys: | |
| 1032 | + * `highlightPhraseOnly`, `highlightFilterStopwords`, `highlightFields`, | |
| 1033 | + * `additionalBlogIds`, `adminQueryFilter`, and `customResults`. | |
| 1034 | + */ | |
| 1035 | + public static function parse_instant_search_query_options( array $options ): array { | |
| 1036 | + $highlight_phrase_only = ! empty( $options['highlightPhraseOnly'] ); | |
| 1037 | + | |
| 1038 | + // The v1.3 API rejects requests combining `highlight_filter_stopwords` with | |
| 1039 | + // `highlight_phrase_only` (phrase-only wins server-side) — drop the stopword | |
| 1040 | + // list so requests stay valid when a filter sets both. | |
| 1041 | + $stopwords = array(); | |
| 1042 | + if ( ! $highlight_phrase_only && ! empty( $options['highlightFilterStopwords'] ) && is_array( $options['highlightFilterStopwords'] ) ) { | |
| 1043 | + $stopwords = array_values( | |
| 1044 | + array_filter( | |
| 1045 | + $options['highlightFilterStopwords'], | |
| 1046 | + static function ( $word ) { | |
| 1047 | + return is_string( $word ) && '' !== $word; | |
| 1048 | + } | |
| 1049 | + ) | |
| 1050 | + ); | |
| 1051 | + } | |
| 1052 | + | |
| 1053 | + $highlight_fields = null; | |
| 1054 | + if ( ! empty( $options['highlightFields'] ) && is_array( $options['highlightFields'] ) ) { | |
| 1055 | + $highlight_fields = array_values( | |
| 1056 | + array_filter( | |
| 1057 | + $options['highlightFields'], | |
| 1058 | + static function ( $field ) { | |
| 1059 | + return is_string( $field ) && '' !== $field; | |
| 1060 | + } | |
| 1061 | + ) | |
| 1062 | + ); | |
| 1063 | + if ( array() === $highlight_fields ) { | |
| 1064 | + $highlight_fields = null; | |
| 1065 | + } | |
| 1066 | + } | |
| 1067 | + | |
| 1068 | + $additional_blog_ids = array(); | |
| 1069 | + if ( ! empty( $options['additionalBlogIds'] ) && is_array( $options['additionalBlogIds'] ) ) { | |
| 1070 | + $additional_blog_ids = array_values( $options['additionalBlogIds'] ); | |
| 1071 | + } | |
| 1072 | + | |
| 1073 | + $admin_query_filter = null; | |
| 1074 | + if ( ! empty( $options['adminQueryFilter'] ) && is_array( $options['adminQueryFilter'] ) ) { | |
| 1075 | + $admin_query_filter = $options['adminQueryFilter']; | |
| 1076 | + } | |
| 1077 | + | |
| 1078 | + $custom_results = array(); | |
| 1079 | + if ( ! empty( $options['customResults'] ) && is_array( $options['customResults'] ) ) { | |
| 1080 | + foreach ( $options['customResults'] as $rule ) { | |
| 1081 | + if ( ! is_array( $rule ) ) { | |
| 1082 | + continue; | |
| 1083 | + } | |
| 1084 | + $pattern = isset( $rule['pattern'] ) && is_string( $rule['pattern'] ) ? $rule['pattern'] : ''; | |
| 1085 | + $ids = isset( $rule['ids'] ) && is_array( $rule['ids'] ) ? array_values( $rule['ids'] ) : array(); | |
| 1086 | + if ( '' === $pattern || array() === $ids ) { | |
| 1087 | + continue; | |
| 1088 | + } | |
| 1089 | + $custom_results[] = array( | |
| 1090 | + 'pattern' => $pattern, | |
| 1091 | + 'ids' => $ids, | |
| 1092 | + ); | |
| 1093 | + } | |
| 1094 | + } | |
| 1095 | + | |
| 1096 | + return array( | |
| 1097 | + 'highlightPhraseOnly' => $highlight_phrase_only, | |
| 1098 | + 'highlightFilterStopwords' => $stopwords, | |
| 1099 | + 'highlightFields' => $highlight_fields, | |
| 1100 | + 'additionalBlogIds' => $additional_blog_ids, | |
| 1101 | + 'adminQueryFilter' => $admin_query_filter, | |
| 1102 | + 'customResults' => $custom_results, | |
| 1103 | + ); | |
| 1104 | + } | |
| 1105 | + | |
| 1106 | + /** | |
| 1107 | + * Read Instant Search query-customization options via `jetpack_instant_search_options`. | |
| 1108 | + * | |
| 1109 | + * Passing an empty array into the filter matches `Filter_Static::read_raw_entries()` | |
| 1110 | + * — callbacks only add keys. | |
| 1111 | + * | |
| 1112 | + * @since 7.4.0 | |
| 1113 | + * | |
| 1114 | + * @return array Query options with keys: | |
| 1115 | + * `highlightPhraseOnly`, `highlightFilterStopwords`, `highlightFields`, | |
| 1116 | + * `additionalBlogIds`, `adminQueryFilter`, and `customResults`. | |
| 1117 | + */ | |
| 1118 | + public static function get_instant_search_query_options(): array { | |
| 1119 | + $options = apply_filters( 'jetpack_instant_search_options', array() ); | |
| 1120 | + if ( ! is_array( $options ) ) { | |
| 1121 | + $options = array(); | |
| 1122 | + } | |
| 1123 | + | |
| 1124 | + return self::parse_instant_search_query_options( $options ); | |
| 1125 | + } | |
| 1126 | + | |
| 1127 | + /** | |
| 1128 | + * Merge Instant Search query-customization options into v1.3 API args. | |
| 1129 | + * | |
| 1130 | + * Shared by Inline Search (Theme) and any server-side v1.3 callers. | |
| 1131 | + * | |
| 1132 | + * @since 7.4.0 | |
| 1133 | + * | |
| 1134 | + * @param array $api_query_args API query arguments. | |
| 1135 | + * @param array|null $options Filtered `jetpack_instant_search_options` value. Defaults to `generate_initial_javascript_state()`. | |
| 1136 | + * @return array | |
| 1137 | + */ | |
| 1138 | + public static function apply_instant_search_query_options_to_api_args( array $api_query_args, ?array $options = null ): array { | |
| 1139 | + if ( null === $options ) { | |
| 1140 | + $options = self::generate_initial_javascript_state(); | |
| 1141 | + } | |
| 1142 | + if ( ! is_array( $options ) ) { | |
| 1143 | + return $api_query_args; | |
| 1144 | + } | |
| 1145 | + | |
| 1146 | + $query_options = self::parse_instant_search_query_options( $options ); | |
| 1147 | + | |
| 1148 | + if ( null !== $query_options['adminQueryFilter'] ) { | |
| 1149 | + $api_query_args['filter'] = array( | |
| 1150 | + 'bool' => array( | |
| 1151 | + 'filter' => $api_query_args['filter'] ?? array(), | |
| 1152 | + 'must' => $query_options['adminQueryFilter'], | |
| 1153 | + ), | |
| 1154 | + ); | |
| 1155 | + } | |
| 1156 | + | |
| 1157 | + if ( $query_options['highlightPhraseOnly'] ) { | |
| 1158 | + $api_query_args['highlight_phrase_only'] = true; | |
| 1159 | + } | |
| 1160 | + | |
| 1161 | + if ( ! empty( $query_options['highlightFilterStopwords'] ) ) { | |
| 1162 | + $api_query_args['highlight_filter_stopwords'] = $query_options['highlightFilterStopwords']; | |
| 1163 | + } | |
| 1164 | + | |
| 1165 | + if ( null !== $query_options['highlightFields'] ) { | |
| 1166 | + $api_query_args['highlight_fields'] = $query_options['highlightFields']; | |
| 1167 | + $api_query_args['highlight'] = array( | |
| 1168 | + 'fields' => $query_options['highlightFields'], | |
| 1169 | + ); | |
| 1170 | + } | |
| 1171 | + | |
| 1172 | + if ( ! empty( $query_options['additionalBlogIds'] ) ) { | |
| 1173 | + $api_query_args['additional_blog_ids'] = $query_options['additionalBlogIds']; | |
| 1174 | + $fields = isset( $api_query_args['fields'] ) && is_array( $api_query_args['fields'] ) | |
| 1175 | + ? $api_query_args['fields'] | |
| 1176 | + : array(); | |
| 1177 | + $api_query_args['fields'] = array_values( | |
| 1178 | + array_unique( array_merge( $fields, self::MULTISITE_SEARCH_FIELD_NAMES ) ) | |
| 1179 | + ); | |
| 1180 | + } | |
| 1181 | + | |
| 1182 | + $matched_custom_results = self::resolve_instant_search_custom_results( | |
| 1183 | + (string) ( $api_query_args['query'] ?? '' ), | |
| 1184 | + $query_options['customResults'] | |
| 1185 | + ); | |
| 1186 | + if ( null !== $matched_custom_results ) { | |
| 1187 | + $api_query_args['custom_results'] = $matched_custom_results; | |
| 1188 | + } | |
| 1189 | + | |
| 1190 | + return $api_query_args; | |
| 1191 | + } | |
| 1192 | + | |
| 1193 | + /** | |
| 1194 | + * Resolve `customResults` post IDs for a search query. | |
| 1195 | + * | |
| 1196 | + * Mirrors instant search's exact / `regex:` pattern matching. | |
| 1197 | + * | |
| 1198 | + * @since 7.4.0 | |
| 1199 | + * | |
| 1200 | + * @param string $query Current search query. | |
| 1201 | + * @param array $custom_results Normalized custom-results rules. | |
| 1202 | + * @return array<int, int|string>|null | |
| 1203 | + */ | |
| 1204 | + public static function resolve_instant_search_custom_results( string $query, array $custom_results ): ?array { | |
| 1205 | + if ( array() === $custom_results ) { | |
| 1206 | + return null; | |
| 1207 | + } | |
| 1208 | + | |
| 1209 | + foreach ( $custom_results as $rule ) { | |
| 1210 | + $pattern = $rule['pattern']; | |
| 1211 | + $ids = $rule['ids']; | |
| 1212 | + if ( 0 === strpos( $pattern, 'regex:' ) ) { | |
| 1213 | + // Escape the delimiter so patterns containing `/` (e.g. `regex:docs/.*`) stay valid PCRE. | |
| 1214 | + $regex = '/^' . str_replace( '/', '\/', substr( $pattern, strlen( 'regex:' ) ) ) . '$/'; | |
| 1215 | + if ( @preg_match( $regex, $query ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Invalid user regex should be skipped, not fatal. | |
| 1216 | + return $ids; | |
| 1217 | + } | |
| 1218 | + } elseif ( $query === $pattern ) { | |
| 1219 | + return $ids; | |
| 1220 | + } | |
| 1221 | + } | |
| 1222 | + | |
| 1223 | + return null; | |
| 1224 | + } | |
| 1225 | + | |
| 1226 | + /** | |
| 899 | 1227 | * Returns true if the site is a WordPress.com simple site, i.e. the code runs on WPCOM. |
| 900 | 1228 | */ |
| 901 | 1229 | public static function is_wpcom() { |
| 902 | 1230 | return defined( 'IS_WPCOM' ) && constant( 'IS_WPCOM' ); |
| @@ -966,6 +1294,28 @@ | ||
| 966 | 1294 | public static function is_forced_new_pricing_202208() { |
| 967 | 1295 | $referrer = wp_get_referer(); |
| 968 | 1296 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash |
| 969 | 1297 | return ( isset( $_GET['new_pricing_202208'] ) && $_GET['new_pricing_202208'] ) || $referrer && strpos( $referrer, 'new_pricing_202208=1' ) !== false; |
| 1298 | + } | |
| 1299 | + | |
| 1300 | + /** | |
| 1301 | + * Returns true if tracking should be disabled via URL parameter, which is used for testing purposes. | |
| 1302 | + * | |
| 1303 | + * @since 0.56.0 | |
| 1304 | + * | |
| 1305 | + * @return bool | |
| 1306 | + */ | |
| 1307 | + public static function is_tracking_disabled() { | |
| 1308 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash | |
| 1309 | + return isset( $_GET['disable_tracking'] ) && $_GET['disable_tracking']; | |
| 1310 | + } | |
| 1311 | + | |
| 1312 | + /** | |
| 1313 | + * Enqueue the WordPress.com Tracks library that drains `window._tkq` and | |
| 1314 | + * sends the queued events. Shared by Instant Search and the Search blocks so | |
| 1315 | + * the handle, src, and cache-busting version live in one place. Callers own | |
| 1316 | + * the decision of whether to load it (e.g. the `is_tracking_disabled()` gate). | |
| 1317 | + */ | |
| 1318 | + public static function enqueue_tracks_script() { | |
| 1319 | + wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true ); | |
| 970 | 1320 | } |
| 971 | 1321 | } |