diff --git a/includes/class-wp-job-manager-post-types.php b/includes/class-wp-job-manager-post-types.php index daf98ad0b..3d094140d 100644 --- a/includes/class-wp-job-manager-post-types.php +++ b/includes/class-wp-job-manager-post-types.php @@ -792,7 +792,7 @@ public function job_feed() { if ( ! empty( $job_manager_keyword ) ) { $query_args['s'] = $job_manager_keyword; - add_filter( 'posts_search', 'get_job_listings_keyword_search' ); + add_filter( 'posts_search', 'get_job_listings_keyword_search', 10, 2 ); } if ( empty( $query_args['meta_query'] ) ) { @@ -808,7 +808,7 @@ public function job_feed() { add_action( 'rss2_ns', [ $this, 'job_feed_namespace' ] ); add_action( 'rss2_item', [ $this, 'job_feed_item' ] ); do_feed_rss2( false ); - remove_filter( 'posts_search', 'get_job_listings_keyword_search' ); + remove_filter( 'posts_search', 'get_job_listings_keyword_search', 10 ); } /** diff --git a/wp-job-manager-functions.php b/wp-job-manager-functions.php index 82a39c32c..301c47629 100644 --- a/wp-job-manager-functions.php +++ b/wp-job-manager-functions.php @@ -196,7 +196,7 @@ function get_job_listings( $args = [] ) { if ( ! empty( $job_manager_keyword ) && strlen( $job_manager_keyword ) >= apply_filters( 'job_manager_get_listings_keyword_length_threshold', 2 ) ) { $query_args['s'] = $job_manager_keyword; - add_filter( 'posts_search', 'get_job_listings_keyword_search' ); + add_filter( 'posts_search', 'get_job_listings_keyword_search', 10, 2 ); } $query_args = apply_filters( 'job_manager_get_listings', $query_args, $args ); @@ -266,7 +266,7 @@ function get_job_listings( $args = [] ) { do_action( 'after_get_job_listings', $query_args, $args ); - remove_filter( 'posts_search', 'get_job_listings_keyword_search' ); + remove_filter( 'posts_search', 'get_job_listings_keyword_search', 10 ); return $result; } @@ -303,66 +303,178 @@ function _wpjm_shuffle_featured_post_results_helper( $a, $b ) { * @since 1.21.0 * @since 1.26.0 Moved from the `posts_clauses` filter to the `posts_search` to use WP Query's keyword * search for `post_title` and `post_content`. - * @param string $search + * @since $$next-version$$ Reimplemented to provide the same functionality with WP core search: + * - Support for double quotes and negating terms (-). + * - Breaks down terms into individual words. + * - Meta and taxonomy name search happens together with search in title, excerpt and post content. + * + * @param string $search The search string. + * @param WP_Query $wp_query The query. + * * @return string */ - function get_job_listings_keyword_search( $search ) { - global $wpdb, $job_manager_keyword; - - // Searchable Meta Keys: set to empty to search all meta keys. - $searchable_meta_keys = [ - '_job_location', - '_company_name', - '_application', - '_company_name', - '_company_tagline', - '_company_website', - '_company_twitter', - ]; + function get_job_listings_keyword_search( $search, $wp_query ) { + global $wpdb; + + if ( ! function_exists( 'job_manager_construct_secondary_conditions' ) && ! function_exists( 'job_manager_construct_post_conditions' ) ) { + /** + * Constructs SQL clauses that return posts which have metas and terms that include or exclude the search term. + * + * @param string $search_term The search term. + * @param bool $is_excluding Whether posts should be excluded if they match the search terms. + * @param string $wildcard_search The wildcard character or empty string for exact matches. + * + * @return array The SQL clauses. + */ + function job_manager_construct_secondary_conditions( $search_term, $is_excluding, $wildcard_search ) { + global $wpdb; + + if ( empty( $search_term ) ) { + return []; + } - $searchable_meta_keys = apply_filters( 'job_listing_searchable_meta_keys', $searchable_meta_keys ); + $searchable_meta_keys = [ + '_application', + '_company_name', + '_company_tagline', + '_company_website', + '_company_twitter', + '_job_location', + ]; - // Set Search DB Conditions. - $conditions = []; + /** + * Filters the meta keys that are used in job search. + * + * @param array $searchable_meta_keys The meta keys. + */ + $searchable_meta_keys = apply_filters( 'job_listing_searchable_meta_keys', $searchable_meta_keys ); + + $not_string = $is_excluding ? 'NOT ' : ''; + $conditions = []; + $meta_value = $wildcard_search . $wpdb->esc_like( $search_term ) . $wildcard_search; + + /** + * Can be used to disable searching post meta for job searches. + * + * @param bool $enable_meta_search Return false to disable meta search. + */ + if ( apply_filters( 'job_listing_search_post_meta', true ) ) { + + // Only selected meta keys. + if ( $searchable_meta_keys ) { + $meta_keys = implode( "','", array_map( 'esc_sql', $searchable_meta_keys ) ); + //phpcs:disabled WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Variables are safe or escaped. + $conditions[] = $wpdb->prepare( "{$wpdb->posts}.ID {$not_string}IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key IN ( '${meta_keys}' ) AND meta_value LIKE %s )", $meta_value ); + } else { + // No meta keys defined, search all post meta value. + $conditions[] = $wpdb->prepare( "{$wpdb->posts}.ID {$not_string}IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_value LIKE %s )", $meta_value ); + //phpcs:enabled WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + } - // Search Post Meta. - if ( apply_filters( 'job_listing_search_post_meta', true ) ) { + // Search taxonomy. + $conditions[] = $wpdb->prepare( "{$wpdb->posts}.ID ${not_string}IN ( SELECT object_id FROM {$wpdb->term_relationships} AS tr LEFT JOIN {$wpdb->term_taxonomy} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id LEFT JOIN {$wpdb->terms} AS t ON tt.term_id = t.term_id WHERE t.name LIKE %s )", $meta_value ); - // Only selected meta keys. - if ( $searchable_meta_keys ) { - $conditions[] = "{$wpdb->posts}.ID IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key IN ( '" . implode( "','", array_map( 'esc_sql', $searchable_meta_keys ) ) . "' ) AND meta_value LIKE '%" . esc_sql( $job_manager_keyword ) . "%' )"; - } else { - // No meta keys defined, search all post meta value. - $conditions[] = "{$wpdb->posts}.ID IN ( SELECT post_id FROM {$wpdb->postmeta} WHERE meta_value LIKE '%" . esc_sql( $job_manager_keyword ) . "%' )"; + return $conditions; } - } - // Search taxonomy. - $conditions[] = "{$wpdb->posts}.ID IN ( SELECT object_id FROM {$wpdb->term_relationships} AS tr LEFT JOIN {$wpdb->term_taxonomy} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id LEFT JOIN {$wpdb->terms} AS t ON tt.term_id = t.term_id WHERE t.name LIKE '%" . esc_sql( $job_manager_keyword ) . "%' )"; + /** + * Constructs SQL clauses that return posts which include or exclude the search term in the provided columns. + * The function replicates the functionality of WP_Query::parse_search. + * + * @see WP_Query::parse_search() + * + * @param string $search_term The search term to match. + * @param bool $is_excluding Whether posts that match the search term should be excluded. + * @param string $wildcard_search The wildcard character or empty string for exact matches. + * @param array $search_columns The columns to check. + * + * @return array The SQL clauses. + */ + function job_manager_construct_post_conditions( $search_term, $is_excluding, $wildcard_search, $search_columns ) { + global $wpdb; + + if ( $is_excluding ) { + $like_op = 'NOT LIKE'; + } else { + $like_op = 'LIKE'; + } + + $like = $wildcard_search . $wpdb->esc_like( $search_term ) . $wildcard_search; + + $conditions = []; + foreach ( $search_columns as $search_column ) { + $search_column = esc_sql( $search_column ); + //phpcs:disabled WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Variables are safe or escaped. + $conditions[] = $wpdb->prepare( "( {$wpdb->posts}.$search_column $like_op %s )", $like ); + } + + // Filter documented in WP_Query::get_posts. + $allow_query_attachment_by_filename = apply_filters( 'wp_allow_query_attachment_by_filename', false ); + if ( ! empty( $allow_query_attachment_by_filename ) ) { + // sq1 is the wp_postmeta join for attachments in WP_Query::get_posts. + $conditions[] = $wpdb->prepare( "(sq1.meta_value $like_op %s)", $like ); + //phpcs:enabled WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + + return $conditions; + } + } /** - * Filters the conditions to use when querying job listings. Resulting array is joined with OR statements. - * - * @since 1.26.0 - * - * @param array $conditions Conditions to join by OR when querying job listings. - * @param string $job_manager_keyword Search query. + * This function aims to provide similar search functionality with WP core while also including meta and taxonomy terms + * in the searched columns. The functionality of WP_Query::parse_search is replicated but with additional SQL + * clauses which are generated in the job_manager_construct_secondary_conditions function. */ - $conditions = apply_filters( 'job_listing_search_conditions', $conditions, $job_manager_keyword ); - if ( empty( $conditions ) ) { - return $search; + $default_search_columns = [ 'post_title', 'post_excerpt', 'post_content' ]; + $search_columns = ! empty( $wp_query->query_vars['search_columns'] ) ? $wp_query->query_vars['search_columns'] : $default_search_columns; + if ( ! is_array( $search_columns ) ) { + $search_columns = [ $search_columns ]; } - $conditions_str = implode( ' OR ', $conditions ); + // Filter documented in WP_Query::parse_search. + $search_columns = (array) apply_filters( 'post_search_columns', $search_columns, $wp_query->query_vars['s'], $wp_query ); + + // Use only supported search columns. + $search_columns = array_intersect( $search_columns, $default_search_columns ); + if ( empty( $search_columns ) ) { + $search_columns = $default_search_columns; + } + + // Search terms starting with the exclusion prefix should be removed from the job search results. + $exclusion_prefix = apply_filters( 'wp_query_search_exclusion_prefix', '-' ); + $wildcard_search = ! empty( $wp_query->query_vars['exact'] ) ? '' : '%'; + $new_search = ''; + $searchand = ''; + + foreach ( $wp_query->query_vars['search_terms'] as $search_term ) { + $is_excluding = $exclusion_prefix && str_starts_with( $search_term, $exclusion_prefix ); - if ( ! empty( $search ) ) { - $search = preg_replace( '/^ AND /', '', $search ); - $search = " AND ( {$search} OR ( {$conditions_str} ) )"; + if ( $is_excluding ) { + $search_term = substr( $search_term, 1 ); + $andor_op = 'AND'; + } else { + $andor_op = 'OR'; + } + + $conditions = job_manager_construct_post_conditions( $search_term, $is_excluding, $wildcard_search, $search_columns ); + $conditions = array_merge( $conditions, job_manager_construct_secondary_conditions( $search_term, $is_excluding, $wildcard_search ) ); + + $new_search .= "$searchand(" . implode( " $andor_op ", $conditions ) . ')'; + + $searchand = ' AND '; + } + + if ( ! empty( $new_search ) ) { + $new_search = " AND ({$new_search}) "; + if ( ! is_user_logged_in() ) { + $new_search .= " AND ({$wpdb->posts}.post_password = '') "; + } } else { - $search = " AND ( {$conditions_str} )"; + return $search; } - return $search; + return $new_search; } endif;