| Server IP : 23.111.136.34 / Your IP : 216.73.216.136 Web Server : Apache System : Linux servidor.eurohost.com.br 3.10.0-1160.119.1.el7.x86_64 #1 SMP Tue Jun 4 14:43:51 UTC 2024 x86_64 User : meusitei ( 1072) PHP Version : 5.6.40 Disable Function : show_source, system, shell_exec, passthru, proc_open MySQL : ON | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : ON Directory : /home/meusitei/public_html/wp-content/plugins/searchwp/includes/ |
Upload File : |
<?php
if ( ! defined( 'ABSPATH' ) ) {
die();
}
/**
* Singleton reference
*/
global $searchwp;
/**
* Class SearchWPSearch performs search queries on the index
*/
class SearchWPSearch {
/**
* @var string Search engine name
* @since 1.0
*/
public $engine;
/**
* @var array Terms to search for
* @since 1.0
*/
public $terms;
/**
* @var mixed|void Stored SearchWP settings
* @since 1.0
*/
public $settings;
/**
* @var int The page of results to work with
* @since 1.0
*/
public $page;
/**
* @var int The number of posts per page
* @since 1.0
*/
public $postsPer;
/**
* @var string The order in which results should be returned
* @since 1.0
*/
public $order = 'DESC';
/**
* @var int Total number of posts found after performing search
*/
public $foundPosts = 0;
/**
* @var int Total number of pages of results
*/
public $maxNumPages = 0;
/**
* @var array Post ID storage
*/
public $postIDs = array();
/**
* @var array Post object storage
*/
public $posts;
/**
* @var string|array post status(es) to include when indexing
*
* @since 1.6.10
*/
public $post_statuses = 'publish';
/**
* @var SearchWP parent
* @since 1.8
*/
public $searchwp;
/**
* @var array engine settings
* @since 1.8
*/
public $engineSettings;
/**
* @var SearchWPStemmer Core keyword stemmer
* @since 1.8
*/
public $stemmer;
/**
* @var array Excluded post IDs
* @since 1.8
*/
public $excluded = array();
/**
* @var array Included post IDs
* @since 1.8
*/
public $included = array();
/**
* @var array Persistent relevant post IDs after various filtration
* @since 1.8
*/
public $relevant_post_ids = array();
/**
* @var string Core database prefix
* @since 1.8
*/
public $db_prefix;
/**
* @var string The main search query
* @since 1.8
*/
public $sql;
/**
* @var string Arbitrary status SQL for the main query
* @since 1.8
*/
public $sql_status;
/**
* @var string JOIN SQL used throughout the query
* @since 1.8
*/
public $sql_join;
/**
* @var string Arbitrary SQL conditions used throughout the query
* @since 1.8
*/
public $sql_conditions;
/**
* @var string Arbitrary WHERE clause used throughout the query
* @since 1.8
*/
public $sql_term_where;
/**
* @var string Generated SQL based on post IDs to include
* @since 1.8
*/
public $sql_include;
/**
* @var string Generated SQL based on post IDs to exclude
* @since 1.8
*/
public $sql_exclude;
/**
* @var array Store the (potentially) filtered terms to save on redundant queries
* @since 2.3
*/
public $terms_final = array();
/**
* @var array Exact weights of returned results
* @since 2.3
*/
public $results_weights = array();
public $tax_count = 0;
public $meta_count = 0;
/**
* Constructor
*
* @param array $args
* @since 1.0
*/
function __construct( $args = array() ) {
global $wpdb, $searchwp;
do_action( 'searchwp_log', 'SearchWPSearch __construct()' );
$defaults = array(
'terms' => '',
'engine' => 'default',
'page' => 1,
'posts_per_page' => intval( get_option( 'posts_per_page' ) ),
'order' => $this->order,
'load_posts' => true,
);
$this->db_prefix = $wpdb->prefix . SEARCHWP_DBPREFIX;
// process our arguments
$args = apply_filters( 'searchwp_search_args', wp_parse_args( $args, $defaults ) );
$this->searchwp = SearchWP::instance();
// instantiate our stemmer for later
$this->stemmer = new SearchWPStemmer();
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', '$args = ' . var_export( $args, true ) );
}
// if we have a valid engine, perform the query
if ( $this->searchwp->is_valid_engine( $args['engine'] ) ) {
// this filter is also applied in the SearchWP class search methods
// TODO: should this be applied in both places? which?
$sanitizeTerms = apply_filters( 'searchwp_sanitize_terms', true, $args['engine'] );
if ( ! is_bool( $sanitizeTerms ) ) {
$sanitizeTerms = true;
}
// whitelist search terms
$pre_whitelist_terms = is_array( $args['terms'] ) ? implode( ' ', $args['terms'] ) : $args['terms'];
$whitelisted_terms = $this->searchwp->extract_terms_using_pattern_whitelist( $pre_whitelist_terms, false );
// TODO: if $whitelisted_terms has matches with spaces, there will be dupes: do we need to loop through and remove?
// store the original search query (e.g. logging)
$pre_search_original_terms = '';
if ( ! empty( $searchwp->original_query ) ) {
$pre_search_original_terms = trim( $searchwp->original_query );
} elseif ( ! empty( $args['terms'] ) ) {
// might have been instantiated directly, use the terms from the args
$pre_search_original_terms = is_array( $args['terms'] ) ? implode( ' ', $args['terms'] ) : $args['terms'];
$pre_search_original_terms = trim( $pre_search_original_terms );
}
if ( $sanitizeTerms ) {
$terms = $this->searchwp->sanitize_terms( $args['terms'], $args['engine'] );
} else {
$terms = $args['terms'];
do_action( 'searchwp_log', 'Opted out of internal sanitization' );
}
if ( is_array( $whitelisted_terms ) ) {
$whitelisted_terms = array_filter( array_map( 'trim', $whitelisted_terms ), 'strlen' );
}
if ( is_array( $terms ) ) {
$terms = array_filter( array_map( 'trim', $terms ), 'strlen' );
$terms = array_merge( $terms, $whitelisted_terms );
} else {
$terms .= ' ' . implode( ' ', $whitelisted_terms );
}
// make sure the terms are unique, especially after whitelist matching
if ( is_array( $terms ) ) {
$terms = array_unique( $terms );
$terms = array_filter( $terms, 'strlen' );
}
$engine = $args['engine'];
// allow dev to customize post statuses are included
$this->post_statuses = (array) apply_filters( 'searchwp_post_statuses', $this->post_statuses, $engine );
foreach ( $this->post_statuses as $post_status_key => $post_status_value ) {
$this->post_statuses[ $post_status_key ] = sanitize_key( $post_status_value );
}
do_action( 'searchwp_log', '$terms = ' . var_export( $terms, true ) );
$args['order'] = strtoupper( apply_filters( 'searchwp_search_query_order', $args['order'] ) );
if ( 'DESC' != $args['order'] && 'ASC' != $args['order'] ) {
$args['order'] = 'DESC';
}
if ( apply_filters( 'searchwp_query_allow_query_string_override_order', true ) ) {
if ( ! empty( $_GET['order'] ) ) {
$args['order'] = 'ASC' == strtoupper( $_GET['order'] ) ? 'ASC' : 'DESC';
}
}
$lenient_accents = apply_filters( 'searchwp_lenient_accents', false );
$lenient_accents_on_search = apply_filters( 'searchwp_lenient_accents_on_search', true );
if ( $lenient_accents && $lenient_accents_on_search && ! empty( $terms ) ) {
$accent_indexer = new SearchWPIndexer();
foreach ( $terms as $term_key => $term ) {
$terms[ $term_key ] = $accent_indexer->remove_accents( $term );
}
}
// filter the terms just before querying
$terms = apply_filters( 'searchwp_pre_search_terms', $terms, $engine );
do_action( 'searchwp_log', 'searchwp_pre_search_terms $terms = ' . var_export( $terms, true ) );
$this->terms = $terms;
$this->engine = $engine;
$this->settings = empty( $searchwp ) ? get_option( SEARCHWP_PREFIX . 'settings' ) : $searchwp->settings;
$this->page = absint( $args['page'] );
$this->postsPer = intval( $args['posts_per_page'] );
$this->order = $args['order'];
$this->load_posts = is_bool( $args['load_posts'] ) ? $args['load_posts'] : true;
$this->offset = ( isset( $args['offset'] ) && ! empty( $args['offset'] ) ) ? absint( $args['offset'] ) : 0;
// pull out our engine-specific settings
$all_settings = SWP()->validate_settings( $this->settings );
if ( ! isset( $all_settings['engines'] ) || ! isset( $all_settings['engines'][ $this->engine ] ) ) {
wp_die( esc_html__( 'Engine settings not found', 'searchwp' ) );
}
$this->engineSettings = $all_settings['engines'][ $this->engine ];
// allow filtration of settings at runtime
$this->engineSettings = apply_filters( "searchwp_engine_settings_{$this->engine}", $this->engineSettings, $this->terms );
// strip out zero weight taxonomies (to reduce as much of the algorithm as possible)
if ( is_array( $this->engineSettings ) && ! empty( $this->engineSettings ) ) {
foreach ( $this->engineSettings as $post_type => $engine_post_type_settings ) {
if ( isset( $engine_post_type_settings['weights']['tax'] ) && is_array( $engine_post_type_settings['weights']['tax'] ) && ! empty( $engine_post_type_settings['weights']['tax'] ) ) {
foreach ( $engine_post_type_settings['weights']['tax'] as $taxonomy_name => $weight ) {
if ( 0 === (int) $weight ) {
unset( $this->engineSettings[ $post_type ]['weights']['tax'][ $taxonomy_name ] );
}
}
}
}
}
// if it's a native search we can piggyback default includes and excludes
if ( is_search() ) {
$this->set_default_include_and_exclude();
}
// we're going to exclude entered IDs for the query as a whole
// need to get these IDs early because if an attributed post ID is excluded we need to omit it from
// the query entirely
$this->set_excluded_ids_from_settings();
// pull any excluded IDs based on taxonomy term
$this->set_excluded_ids_from_taxonomies();
if ( ! empty( $this->terms ) ) {
// perform our query
$this->posts = $this->query();
// log this
$paged = absint( $this->page ) > 1;
// Default is to log if we're not doing an admin column nor are we paging
$log_default = ! $this->doing_admin_column() && ! $paged;
if ( ! empty( $pre_search_original_terms ) && apply_filters( 'searchwp_log_search', $log_default, $engine, $pre_search_original_terms, absint( $this->foundPosts ) ) ) {
$pre_search_original_terms = sanitize_text_field( $pre_search_original_terms );
$pre_search_original_terms = trim( $pre_search_original_terms );
// respect database schema
if ( 200 < strlen( $pre_search_original_terms ) ) {
$pre_search_original_terms = substr( $pre_search_original_terms, 0, 199 );
}
if ( ! empty( $pre_search_original_terms ) ) {
/** @noinspection PhpUnusedLocalVariableInspection */
$log_result = $wpdb->insert(
$this->db_prefix . 'log',
array(
'event' => 'search',
'query' => $pre_search_original_terms,
'hits' => absint( $this->foundPosts ),
'engine' => $engine,
'wpsearch' => 0,
),
array(
'%s',
'%s',
'%d',
'%s',
'%d',
)
);
}
}
}
}
}
/**
* Determine whether we're outputting an admin column
*
* @return bool
*/
function doing_admin_column() {
$doing_admin_column = false;
if ( ! is_admin() ) {
return $doing_admin_column;
}
$post_types = get_post_types();
foreach ( $post_types as $post_type ) {
if ( did_action( 'manage_' . $post_type . '_posts_custom_column' )
|| doing_action( 'manage_' . $post_type . '_posts_custom_column' ) ) {
$doing_admin_column = true;
break;
}
}
return $doing_admin_column;
}
/**
* Getter for currently excluded IDs
*
* @since 2.8.5
*
* @return array
*/
function get_excluded() {
return $this->excluded;
}
/**
* Getter for currently included IDs
*
* @since 2.8.5
*
* @return array
*/
function get_included() {
return $this->included;
}
/**
* Piggyback $wp_query arguments to set better include/exclude defaults
*
* @since 2.5
*/
function set_default_include_and_exclude() {
// We need to ensure that this is in fact the main query, else we can get some
// wacky behavior from other functionality e.g. pre_get_posts hook usage that
// in turn uses WP_Query can get disastrous results
if ( empty( SWP()->isMainQuery ) ) {
return;
}
// set default inclusions (based on $wp_query (other plugins likely do their magic by setting this))
$wp_query_post__in = get_query_var( 'post__in' );
if ( ! empty( $wp_query_post__in ) ) {
if ( ! is_array( $wp_query_post__in ) ) {
$wp_query_post__in = explode( ',', $wp_query_post__in );
}
$this->included = array_map( 'absint', (array) $wp_query_post__in );
do_action( 'searchwp_log', 'Setting default post__in: ' . implode( ', ', $this->included ) );
}
// set default exclusions in the same fashion
$wp_query_post__not_in = get_query_var( 'post__not_in' );
if ( ! empty( $wp_query_post__not_in ) ) {
if ( ! is_array( $wp_query_post__not_in ) ) {
$wp_query_post__not_in = explode( ',', $wp_query_post__not_in );
}
$this->excluded = array_map( 'absint', (array) $wp_query_post__not_in );
do_action( 'searchwp_log', 'Setting default post__not_in: ' . implode( ', ', $this->excluded ) );
}
}
/**
* Perform a query on the index
*
* @return array Posts returned by the query
* @since 1.0
*/
function query() {
do_action( 'searchwp_log', 'query()' );
do_action( 'searchwp_before_query_index', array(
'terms' => $this->terms,
'engine' => $this->engine,
'settings' => $this->settings,
'page' => $this->page,
'postsPer' => $this->postsPer,
) );
$this->query_for_post_ids();
$swpargs = array(
'terms' => $this->terms,
'engine' => $this->engine,
'settings' => $this->settings,
'page' => $this->page,
'postsPer' => $this->postsPer,
);
do_action( 'searchwp_log', 'query() complete' );
do_action( 'searchwp_after_query_index', $swpargs );
// facilitate filtration of returned results
$this->postIDs = apply_filters( 'searchwp_query_results', $this->postIDs, $swpargs );
if ( empty( $this->postIDs ) ) {
return array();
}
// our post IDs will have already been filtered based on the engine settings, so we want to query for
// anything that matches our post IDs
$args = array(
'posts_per_page' => count( $this->postIDs ),
'post_type' => SWP()->get_indexed_post_types(),
'post_status' => 'any', // we've already filtered our post statuses in the original query
'post__in' => $this->postIDs,
'orderby' => 'post__in',
);
// we want ints all the time
$this->postIDs = array_map( 'absint', $this->postIDs );
if ( $this->load_posts && true === apply_filters( 'searchwp_load_posts', true, $swpargs ) ) {
// we don't want anything interfering with us getting our posts
if ( apply_filters( 'searchwp_remove_pre_get_posts_during_search', false ) ) {
remove_all_actions( 'pre_get_posts' );
remove_all_filters( 'pre_get_posts' );
}
$posts = apply_filters( 'searchwp_found_post_objects', get_posts( $args ), $swpargs );
} else {
$posts = $this->postIDs;
}
do_action( 'searchwp_log', 'query() return' );
return $posts;
}
/**
* Ensures that all post types in settings still exist
*
* @since 1.8
*/
public function validate_post_types() {
// devs can customize which post types are indexed, it doesn't make
// sense to list post types that were excluded
$indexed_post_types = apply_filters( 'searchwp_indexed_post_types', $this->searchwp->postTypes );
if ( is_array( $indexed_post_types ) ) {
foreach ( $this->engineSettings as $postType => $postTypeSettings ) {
if ( ! in_array( $postType, $indexed_post_types ) || ! post_type_exists( $postType ) ) {
unset( $this->engineSettings[ $postType ] );
}
}
}
}
/**
* Determine whether any post types are enabled
*
* @return bool Whether there are any enabled post types
* @since 1.8
*/
public function any_enabled_post_types() {
$enabled_post_type = false;
// check to make sure that at least one post type is enabled for this engine
if ( is_array( $this->engineSettings ) ) {
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] ) {
$enabled_post_type = true;
break;
}
}
}
return $enabled_post_type;
}
/**
* Set excluded IDs as per the engine settings
*
* @since 1.8
*/
function set_excluded_ids_from_settings() {
$excludeIDs = apply_filters( 'searchwp_prevent_indexing', $this->excluded ); // catch anything that shouldn't have been indexed anyway
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( empty( $postTypeWeights['enabled'] ) ) {
continue;
}
// store our exclude clause
if ( isset( $postTypeWeights['options']['exclude'] ) && ! empty( $postTypeWeights['options']['exclude'] ) ) {
$postTypeExcludeIDs = $postTypeWeights['options']['exclude'];
// stored as a comma separated string of integers
if ( is_string( $postTypeExcludeIDs ) && false !== strpos( $postTypeExcludeIDs, ',' ) ) {
$postTypeExcludeIDs = explode( ',', $postTypeExcludeIDs );
} else {
if ( is_string( $postTypeExcludeIDs ) ) {
$postTypeExcludeIDs = array( absint( $postTypeExcludeIDs ) );
} else {
$postTypeExcludeIDs = array();
}
}
} else {
$postTypeExcludeIDs = array();
}
if ( ! empty( $postTypeExcludeIDs ) && is_array( $postTypeExcludeIDs ) ) {
foreach ( $postTypeExcludeIDs as $postTypeExcludeID ) {
$excludeIDs[] = absint( $postTypeExcludeID );
}
}
}
if ( ! is_array( $excludeIDs ) ) {
$excludeIDs = array();
} else {
$excludeIDs = array_map( 'absint', $excludeIDs );
}
$excludeIDs = array_unique( $excludeIDs );
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', '$excludeIDs = ' . var_export( $excludeIDs, true ) );
}
$this->excluded = $excludeIDs;
}
/**
* Set excluded IDs based on taxonomy terms in the settings
*
* @since 1.8
*/
function set_excluded_ids_from_taxonomies() {
add_filter( 'searchwp_force_wp_query', '__return_true' ); // we're going to be firing a WP_Query and want it to finish
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( empty( $postTypeWeights['enabled'] ) ) {
continue;
}
$taxonomies = get_object_taxonomies( $postType );
if ( is_array( $taxonomies ) && count( $taxonomies ) ) {
foreach ( $taxonomies as $taxonomy ) {
$taxonomy = get_taxonomy( $taxonomy );
if ( isset( $postTypeWeights['options'][ 'exclude_' . $taxonomy->name ] ) ) {
$excludedTerms = explode( ',', $postTypeWeights['options'][ 'exclude_' . $taxonomy->name ] );
if ( ! is_array( $excludedTerms ) ) {
$excludedTerms = array( intval( $excludedTerms ) );
}
if ( ! empty( $excludedTerms ) ) {
foreach ( $excludedTerms as $excludedKey => $excludedValue ) {
$excludedTerms[ $excludedKey ] = intval( $excludedValue );
}
}
// determine which post(s) have this term
$args = array(
'posts_per_page' => -1,
'fields' => 'ids',
'post_type' => $postType,
'suppress_filters' => true,
'tax_query' => array(
array(
'taxonomy' => $taxonomy->name,
'field' => 'id',
'terms' => $excludedTerms,
),
)
);
// Media won't be published
if ( 'attachment' == $postType ) {
$args['post_status'] = 'inherit';
}
$excludedByTerm = new WP_Query( $args );
if ( ! empty( $excludedByTerm ) ) {
$this->excluded = array_merge( $this->excluded, $excludedByTerm->posts );
}
}
}
}
}
remove_filter( 'searchwp_force_wp_query', '__return_true' );
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', 'After taxonomy exclusion $excludeIDs = ' . var_export( $this->excluded, true ) );
}
}
/**
* Get an array of IDs for posts that have been limited using engine rules
*
* @since 2.9.8
*/
function get_included_ids_from_taxonomies_for_post_type( $post_type = 'post' ) {
if ( ! post_type_exists( $post_type ) ) {
return false;
}
add_filter( 'searchwp_force_wp_query', '__return_true' ); // we're going to be firing a WP_Query and want it to finish
$limited_ids = false;
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( $postType !== $post_type || empty( $postTypeWeights['enabled'] ) ) {
continue;
}
$taxonomies = get_object_taxonomies( $postType );
if ( is_array( $taxonomies ) && count( $taxonomies ) ) {
foreach ( $taxonomies as $taxonomy ) {
$taxonomy = get_taxonomy( $taxonomy );
if ( isset( $postTypeWeights['options'][ 'limit_to_' . $taxonomy->name ] ) ) {
$includedTerms = explode( ',', $postTypeWeights['options'][ 'limit_to_' . $taxonomy->name ] );
if ( ! is_array( $includedTerms ) ) {
$includedTerms = array( intval( $includedTerms ) );
}
if ( ! empty( $includedTerms ) ) {
foreach ( $includedTerms as $includedKey => $includedValue ) {
$includedTerms[ $includedKey ] = intval( $includedValue );
}
}
// determine which post(s) have this term
$args = array(
'posts_per_page' => -1,
'fields' => 'ids',
'post_type' => $postType,
'suppress_filters' => true,
'tax_query' => array(
array(
'taxonomy' => $taxonomy->name,
'field' => 'id',
'terms' => $includedTerms,
),
)
);
// Media won't be published
if ( 'attachment' == $postType ) {
$args['post_status'] = 'inherit';
}
$includedByTerm = new WP_Query( $args );
$limited_ids = $includedByTerm->posts;
break;
}
}
}
break;
}
remove_filter( 'searchwp_force_wp_query', '__return_true' );
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', 'After taxonomy limiter $includeIDS = ' . var_export( $limited_ids, true ) );
}
return $limited_ids;
}
/**
* Set included IDs based on taxonomy terms in the settings
* NOTE: This will return IDs regardless of post type, use with caution
*
* @since 2.9
*/
function set_included_ids_from_taxonomies() {
add_filter( 'searchwp_force_wp_query', '__return_true' ); // we're going to be firing a WP_Query and want it to finish
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( empty( $postTypeWeights['enabled'] ) ) {
continue;
}
$taxonomies = get_object_taxonomies( $postType );
if ( is_array( $taxonomies ) && count( $taxonomies ) ) {
foreach ( $taxonomies as $taxonomy ) {
$taxonomy = get_taxonomy( $taxonomy );
if ( isset( $postTypeWeights['options'][ 'limit_to_' . $taxonomy->name ] ) ) {
$includedTerms = explode( ',', $postTypeWeights['options'][ 'limit_to_' . $taxonomy->name ] );
if ( ! is_array( $includedTerms ) ) {
$includedTerms = array( intval( $includedTerms ) );
}
if ( ! empty( $includedTerms ) ) {
foreach ( $includedTerms as $includedKey => $includedValue ) {
$includedTerms[ $includedKey ] = intval( $includedValue );
}
}
// determine which post(s) have this term
$args = array(
'posts_per_page' => -1,
'fields' => 'ids',
'post_type' => $postType,
'suppress_filters' => true,
'tax_query' => array(
array(
'taxonomy' => $taxonomy->name,
'field' => 'id',
'terms' => $includedTerms,
),
)
);
// Media won't be published
if ( 'attachment' == $postType ) {
$args['post_status'] = 'inherit';
}
$includedByTerm = new WP_Query( $args );
if ( ! empty( $includedByTerm->posts ) ) {
$this->included = array_merge( $this->included, $includedByTerm->posts );
} else {
$this->included = array_merge( $this->included, array( 0 ) );
}
}
}
}
}
remove_filter( 'searchwp_force_wp_query', '__return_true' );
do_action( 'searchwp_log', 'After taxonomy limiter $includeIDS = ' . var_export( $this->included, true ) );
}
/**
* Determine which field types should be considered for AND logic
*
* @since 1.8
*/
public function get_and_fields( $post_type = '' ) {
// If an invalid post type is submitted, revert to global AND fields across all engine post types
if ( ! empty( $post_type ) && ! post_type_exists( $post_type ) ) {
$post_type = '';
}
// allow devs to filter which fields should be included for AND checks
$andFieldsDefaults = array( 'title', 'content', 'slug', 'excerpt', 'comment', 'tax', 'meta' );
// Store which AND fields the engine actually uses
$theseAndFields = array();
// If we're doing a search any default AND field has a weight of zero, it doesn't apply
if ( did_action( 'searchwp_before_query_index' ) ) {
foreach ( $this->settings['engines'][ $this->engine ] as $engine_post_type => $post_type_settings ) {
// If the post type is enabled, it doesn't matter
if ( empty( $post_type_settings['enabled'] ) ) {
continue;
}
// Allow restriction to single post type
if ( ! empty( $post_type ) && $post_type !== $engine_post_type ) {
continue;
}
if ( isset( $post_type_settings['weights'] ) && is_array( $post_type_settings['weights'] ) ) {
foreach ( $post_type_settings['weights'] as $field_type => $weight ) {
// 'cf' is used for Custom Fields in the Settings but it's confusing; it's meta here
if ( 'cf' === $field_type ) {
$field_type = 'meta';
}
if ( in_array( $field_type, $andFieldsDefaults, true ) ) {
if ( is_numeric( $weight ) && ! empty( $weight ) ) {
$theseAndFields[] = $field_type;
} elseif ( is_array( $weight) && ! empty( $weight ) ) {
foreach ( $weight as $kweight ) {
if ( is_numeric( $kweight ) && ! empty( $kweight ) ) {
$theseAndFields[] = $field_type;
break;
} elseif ( is_array( $kweight) && isset( $kweight['weight'] ) ) { // overly complex data model
if ( is_numeric( $kweight['weight'] ) && ! empty( $kweight['weight'] ) ) {
$theseAndFields[] = $field_type;
break;
}
}
}
}
}
}
}
}
$theseAndFields = array_unique( $theseAndFields );
}
$andFields = $theseAndFields;
if ( ! empty( $post_type ) ) {
$andFields = apply_filters( "searchwp_and_fields_{$post_type}", $andFields, $this->engine );
}
$andFields = apply_filters( 'searchwp_and_fields', $andFields );
// validate AND fields
if ( is_array( $andFields ) && ! empty( $andFields ) ) {
$strtolower_function = function_exists( 'mb_strtolower' ) ? 'mb_strtolower' : 'strtolower';
$andFields = array_map( $strtolower_function, $andFields );
foreach ( $andFields as $andFieldKey => $andField ) {
if ( ! in_array( $andField, $andFieldsDefaults, true ) ) {
// invalid field, kill it
unset( $andFields[ $andFieldKey ] );
}
}
} else {
// returned not an array, so reset it (which will basically invalidate AND searching)
$andFields = array();
}
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', '$andFields = ' . implode( ', ', $andFields ) );
}
return $andFields;
}
/**
* Use AND logic to find post IDs that have all search terms
*
* @param $andFields array The AND fields to consider when applying logic
* @param $andTerm string The keyword
*
* @return array The applicable Post IDs
* @since 1.8
*/
public function get_post_ids_via_and( $andFields, $andTerm, $postType = '' ) {
global $wpdb;
// If an invalid post type is submitted, revert to global AND for all post types (not ideal)
if ( ! empty( $postType ) && ! post_type_exists( $postType ) ) {
$postType = '';
}
// we're going to utilize $andFields to build our query based on what the dev wants to count for AND queries
$andFieldsCoalesce = $this->get_and_field_coalesce( $andFields );
// in order to save having to scrub through every enabled post type
// we're just going to assume a stem here and limit the result pool as quickly as possible
// since the main query will take into consideration the additional limitation of the stem
$unstemmed = $andTerm;
$maybeStemmed = apply_filters( 'searchwp_custom_stemmer', $unstemmed );
// if the term was stemmed via the filter use it, else generate our own
$originalAndTerm = ( $unstemmed == $maybeStemmed ) ? $this->stemmer->stem( $andTerm ) : $maybeStemmed;
$andTerm = $wpdb->prepare( '%s', $originalAndTerm );
$andTermLower = function_exists( 'mb_strtolower' ) ? mb_strtolower( $andTerm, 'UTF-8' ) : strtolower( $andTerm );
$relevantTermWhere = " {$this->db_prefix}terms.stem = " . $andTermLower;
// as an optimization we're going to break up this query into three 'parts'
// 1) SELECT against the index table to find out where this term appears at least once
// 2) SELECT against the cf table
// 3) SELECT against the tax table
//
// all three will be UNIONed but all three are also filterable so we need to build this query carefully
// and completely based on $andFields (which is an array of fields to consider)
$andTermSQL = '';
$active_post_types_for_this_engine = array();
if ( empty( $postType ) ) {
foreach ( $this->settings['engines'][ $this->engine ] as $post_type => $settings ) {
if ( ! empty( $settings['enabled'] ) ) {
$active_post_types_for_this_engine[] = $post_type;
}
}
} else {
// Limiting to a single post type was added as a bugfix in 2.9.9 so we're essentially hacking
// the way this function works by limiting to a single post type as though it was the only
// enabled post type for the engine
$active_post_types_for_this_engine = array( $postType );
}
if ( empty( $active_post_types_for_this_engine ) ) {
return array();
}
$clause_count = 0;
$post_parents = array();
// first SQL segment is against the index table
if ( ! empty( $andFieldsCoalesce ) ) {
$clause_count++;
// we do in fact want to run query 1
$andTermSQL .= "
SELECT {$this->db_prefix}index.post_id, {$wpdb->prefix}posts.post_parent,
SUM({$andFieldsCoalesce}) as termcount
FROM {$this->db_prefix}index FORCE INDEX (termindex)
LEFT JOIN {$this->db_prefix}terms
ON {$this->db_prefix}index.term = {$this->db_prefix}terms.id
LEFT JOIN {$wpdb->prefix}posts
ON {$wpdb->prefix}posts.ID = {$this->db_prefix}index.post_id
WHERE {$relevantTermWhere}
AND {$wpdb->prefix}posts.post_type IN ( " . implode( ', ', array_fill( 0, count( $active_post_types_for_this_engine ), '%s' ) ) . " ) GROUP BY post_id HAVING termcount > 0 ";
}
// next SQL segment is against the cf table
if ( in_array( 'meta', $andFields ) ) {
$clause_count++;
if ( ! empty( $andTermSQL ) ) {
$andTermSQL .= ' UNION ';
}
// we want to apply AND logic to the cf table
$andTermSQL .= "
SELECT {$this->db_prefix}cf.post_id, {$wpdb->prefix}posts.post_parent, SUM(`count`) as termcount
FROM {$this->db_prefix}cf FORCE INDEX (term)
LEFT JOIN {$this->db_prefix}terms
ON {$this->db_prefix}cf.term = {$this->db_prefix}terms.id
LEFT JOIN {$wpdb->prefix}posts
ON {$wpdb->prefix}posts.ID = {$this->db_prefix}cf.post_id
WHERE {$relevantTermWhere}
AND {$wpdb->prefix}posts.post_type IN ( " . implode( ', ', array_fill( 0, count( $active_post_types_for_this_engine ), '%s' ) ) . " ) GROUP BY post_id HAVING termcount > 0 ";
}
// last SQL segment is against the tax table
if ( in_array( 'tax', $andFields ) ) {
$clause_count++;
if ( ! empty( $andTermSQL ) ) {
$andTermSQL .= ' UNION ';
}
// we want to apply AND logic to the cf table
$andTermSQL .= "
SELECT {$this->db_prefix}tax.post_id, {$wpdb->prefix}posts.post_parent, SUM(`count`) as termcount
FROM {$this->db_prefix}tax FORCE INDEX (term)
LEFT JOIN {$this->db_prefix}terms
ON {$this->db_prefix}tax.term = {$this->db_prefix}terms.id
LEFT JOIN {$wpdb->prefix}posts
ON {$wpdb->prefix}posts.ID = {$this->db_prefix}tax.post_id
WHERE {$relevantTermWhere}
AND {$wpdb->prefix}posts.post_type IN ( " . implode( ', ', array_fill( 0, count( $active_post_types_for_this_engine ), '%s' ) ) . " ) GROUP BY post_id HAVING termcount > 0";
}
$postsWithTermPresent = array();
$values_to_prepare = array();
for ( $i = 0; $i < $clause_count; $i++ ) {
$values_to_prepare = array_merge( $values_to_prepare, $active_post_types_for_this_engine );
}
$postsWithTermPresentRef = $wpdb->get_results(
$wpdb->prepare(
$andTermSQL,
$values_to_prepare
)
);
// we retrieved both the post ID and the post_parent (to account for attribution) so let's merge them
if ( is_array( $postsWithTermPresentRef ) && ! empty( $postsWithTermPresentRef ) ) {
foreach ( $postsWithTermPresentRef as $post_ref ) {
if ( isset( $post_ref->post_id ) && ! empty( $post_ref->post_id ) ) {
$postsWithTermPresent[] = absint( $post_ref->post_id );
}
if ( isset( $post_ref->post_parent ) && ! empty( $post_ref->post_parent ) ) {
$post_parents[] = absint( $post_ref->post_parent );
}
}
}
if ( ! empty( $post_parents ) ) {
$post_parents = array_map( 'absint', $post_parents );
$post_parents = array_unique( $post_parents );
// Check to make sure the parents in fact have all terms
$searchwp_index_table = $wpdb->prefix . SEARCHWP_DBPREFIX . 'index';
$searchwp_terms_table = $wpdb->prefix . SEARCHWP_DBPREFIX . 'terms';
$sql = "
SELECT {$searchwp_index_table}.id
FROM {$searchwp_index_table}
LEFT JOIN {$searchwp_terms_table}
ON {$searchwp_terms_table}.id = {$searchwp_index_table}.term
WHERE {$searchwp_terms_table}.stem = %s
AND {$searchwp_index_table}.id IN ( " . implode( ', ', array_fill( 0, count( $post_parents ), '%d' ) ) . " )
";
$originalAndTermLower = function_exists( 'mb_strtolower' ) ? mb_strtolower( $originalAndTerm, 'UTF-8' ) : strtolower( $originalAndTerm );
$parent_values_to_prep = array_merge( array( $originalAndTermLower ), $post_parents );
$parent_sql = $wpdb->prepare( trim( $sql ), $parent_values_to_prep );
$parents_with_term = $wpdb->get_col( $parent_sql );
if ( ! empty( $parents_with_term ) ) {
$postsWithTermPresent = array_merge( $postsWithTermPresent, $parents_with_term );
}
}
// even though we're using UNION, we will likely have duplicate post_ids because each row will have different term counts
if ( is_array( $postsWithTermPresent ) && ! empty( $postsWithTermPresent ) ) {
$postsWithTermPresent = array_unique( $postsWithTermPresent );
}
// Make sure to take into account excluded IDs
$postsWithTermPresent = array_diff( $postsWithTermPresent, $this->excluded );
if ( ! empty( $postsWithTermPresent ) ) {
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', 'Algorithm AND logic pass: ' . implode( ', ', $postsWithTermPresent ) );
}
}
return $postsWithTermPresent;
}
/**
* Generate the SQL used in AND field logic
*
* @param $andFields array The AND fields to consider when applying logic
*
* @return string SQL to use in the main query
* @since 1.8
*/
public function get_and_field_coalesce( $andFields ) {
$coalesceFields = array();
// we're going to utilize $andFields to build our query based on what the dev wants to count for AND queries
foreach ( $andFields as $andField ) {
switch ( $andField ) {
case 'tax':
// taxonomy has been broken out into UNION as of version 2.0.5
break;
case 'meta':
// cf has been broken out into UNION as of 2.0.5
break;
default:
$andFieldTable = 'index';
$andFieldColumn = sanitize_text_field( $andField );
$coalesceFields[] = "COALESCE({$this->db_prefix}{$andFieldTable}.{$andFieldColumn},0)";
break;
}
}
if ( ! empty( $coalesceFields ) ) {
$andFieldsCoalesce = implode( ' + ', $coalesceFields );
} else {
$andFieldsCoalesce = '';
}
return $andFieldsCoalesce;
}
/**
* If applicable, limit posts using AND logic
*
* @since 1.8
*/
public function maybe_do_and_logic() {
// AND logic only applies if there's more than one term (and the dev doesn't disable it)
$doAnd = ( count( $this->terms ) > 1 && apply_filters( 'searchwp_and_logic', true ) ) ? true : false;
do_action( 'searchwp_log', '$doAnd = ' . var_export( $doAnd, true ) );
$andTerms = array();
// AND logic is going to be different per post type, so we need to determine relevant IDs across the entire engine
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] ) {
// AND fields need to be defined per post type as well
$and_fields_for_post_type = $this->get_and_fields( $postType );
$andTerms[ $postType ] = array();
if ( $doAnd && is_array( $and_fields_for_post_type ) && ! empty( $and_fields_for_post_type ) ) {
// Assume AND logic is going to happen
$applicableAndResults = true;
// We need to find posts that have all search terms for this post type
foreach ( $this->terms as $andTerm ) {
$postsWithTermPresent = $this->get_post_ids_via_and( $and_fields_for_post_type, $andTerm, $postType );
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', '$postsWithTermPresent (' . $postType . ') = ' . implode( ', ', $postsWithTermPresent ) );
}
$andTerms[ $postType ][] = $postsWithTermPresent;
}
// Current status: Each element of $andTerms[ $postType ] is an array of
// post IDs that contains that term, we need to intersect all of them to ensure AND logic
if ( isset( $andTerms[ $postType ] ) && is_array( $andTerms[ $postType ] ) && count( $andTerms[ $postType ] ) > 1 ) {
$andTerms[ $postType ] = call_user_func_array( 'array_intersect', $andTerms[ $postType ] );
} else {
$andTerms[ $postType ] = array();
}
}
}
}
// $andTerms contains a breakdown of AND results per post type
// If there are are post types with no AND logic matches we can strip those out now
foreach ( $andTerms as $post_type => $potential_and_logic_ids ) {
if ( empty( $potential_and_logic_ids ) ) {
unset( $andTerms[ $post_type ] );
}
}
// We now have a reduced array of potential AND logic IDs per post type,
// but in order to find the IDs that satisfy AND logic, we need to intersect
$relevantPostIds = array();
foreach ( $andTerms as $post_type => $potential_and_logic_ids ) {
if ( ! empty( $potential_and_logic_ids ) ) {
$relevantPostIds = array_merge( $relevantPostIds, $potential_and_logic_ids );
}
}
$relevantPostIds = array_unique( $relevantPostIds );
$this->relevant_post_ids = array_map( 'absint', $relevantPostIds );
}
/**
* If a weight is < 0 any results need to be forcefully excluded
*
* @since 1.8
*/
public function exclude_posts_by_weight() {
global $wpdb;
// we need to check for exclusions at this point (weights of < zero)
$andTerms = array();
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] && count( $postTypeWeights['weights'] ) ) {
foreach ( $postTypeWeights['weights'] as $type => $weight ) {
foreach ( $this->terms as $andTerm ) {
$applicableExclusion = false;
// determine whether we want a term match or stem match
$andTermPrepared = $wpdb->prepare( '%s', $andTerm );
$andTermLower = function_exists( 'mb_strtolower' ) ? mb_strtolower( $andTermPrepared, 'UTF-8' ) : strtolower( $andTermPrepared );
if ( ! isset( $postTypeWeights['options']['stem'] ) || empty( $postTypeWeights['options']['stem'] ) ) {
$relavantTermWhere = " {$this->db_prefix}terms.term = " . $andTermLower;
} else {
$unstemmed = $andTerm;
$maybeStemmed = apply_filters( 'searchwp_custom_stemmer', $unstemmed );
// if the term was stemmed via the filter use it, else generate our own
$andTerm = ( $unstemmed == $maybeStemmed ) ? $this->stemmer->stem( $andTerm ) : $maybeStemmed;
$relavantTermWhere = " {$this->db_prefix}terms.stem = " . $wpdb->prepare( '%s', $andTerm );
}
$andInternalSQL = "
SELECT {$this->db_prefix}index.post_id
FROM {$this->db_prefix}index
LEFT JOIN {$wpdb->posts}
ON {$this->db_prefix}index.post_id = {$wpdb->posts}.ID
LEFT JOIN {$this->db_prefix}terms
ON {$this->db_prefix}index.term = {$this->db_prefix}terms.id
LEFT JOIN {$this->db_prefix}cf
ON {$this->db_prefix}index.post_id = {$this->db_prefix}cf.post_id
LEFT JOIN {$this->db_prefix}tax
ON {$this->db_prefix}index.post_id = {$this->db_prefix}tax.post_id
WHERE {$relavantTermWhere} ";
if ( ! empty( $this->relevant_post_ids ) ) {
$this->relevant_post_ids = array_map( 'absint', $this->relevant_post_ids );
$relevantIDsSQL = implode( ',', $this->relevant_post_ids );
$andInternalSQL .= " AND {$this->db_prefix}index.post_id IN ({$relevantIDsSQL}) ";
}
$andInternalSQL .= ' AND ( ';
// $weight will sometimes be an array (taxonomies and custom fields)
if ( ! is_array( $weight ) && intval( $weight ) < 0 ) {
$applicableExclusion = true;
switch ( $type ) {
case 'title':
$andInternalSQL .= " {$this->db_prefix}index.title > 0 OR ";
break;
case 'content':
$andInternalSQL .= " {$this->db_prefix}index.content > 0 OR ";
break;
case 'slug':
$andInternalSQL .= " {$this->db_prefix}index.slug > 0 OR ";
break;
case 'excerpt':
$andInternalSQL .= " {$this->db_prefix}index.excerpt > 0 OR ";
break;
case 'comment':
$andInternalSQL .= " {$this->db_prefix}index.comment > 0 OR ";
break;
}
} else {
// it's either a taxonomy or custom field, so we need to handle it a bit differently
if ( 'tax' == $type ) {
foreach ( $weight as $postTypeTax => $postTypeTaxWeight ) {
if ( intval( $postTypeTaxWeight ) < 0 ) {
$applicableExclusion = true;
// taxonomy name has already been validated by always safest to escape
if ( ! taxonomy_exists( $postTypeTax ) ) {
wp_die( 'Invalid request', 'searchwp' );
}
$postTypeTax = $wpdb->prepare( '%s', $postTypeTax );
$andInternalSQL .= " ( {$this->db_prefix}tax.taxonomy = {$postTypeTax} AND {$this->db_prefix}tax.count > 0 ) OR ";
}
}
} elseif ( 'cf' == $type ) {
foreach ( $weight as $postTypeCustomField ) {
if ( ! is_array( $postTypeCustomField ) ) {
continue;
}
if ( isset( $postTypeCustomField['weight'] ) ) {
$postTypeCustomFieldWeight = $postTypeCustomField['weight'];
}
if ( isset( $postTypeCustomField['metakey'] ) ) {
$postTypeCustomFieldMetakey = $postTypeCustomField['metakey'];
}
if ( intval( $postTypeCustomFieldWeight ) < 0 ) {
$applicableExclusion = true;
// field name has already been validated by always safest to escape
$postTypeCustomFieldKey = $wpdb->prepare( '%s', $postTypeCustomFieldMetakey );
$andInternalSQL .= " ( {$this->db_prefix}cf.metakey = {$postTypeCustomFieldKey} AND {$this->db_prefix}cf.count > 0 ) OR ";
}
}
}
}
// trim off the extra OR
$andInternalSQL = substr( $andInternalSQL, 0, strlen( $andInternalSQL ) - 4 ) . " ) AND {$wpdb->posts}.post_type = '{$postType}' GROUP BY {$this->db_prefix}index.post_id";
// if this exclusion is applicable, grab post IDs that trigger the exclusion
if ( $applicableExclusion ) {
$postsWithTerm = $wpdb->get_col( $andInternalSQL );
// add these post IDs to the heap (we're going to make it unique later)
$andTerms = array_merge( $andTerms, array_map( 'absint', $postsWithTerm ) );
}
}
}
}
}
// $andTerms is a conglomerate pile of post IDs violating the exclusion rule
$andTerms = array_unique( $andTerms );
// merge the weight-based exlusions on to the main excludes
$excludeIDs = array_merge( $this->excluded, $andTerms );
// make sure everything is an int
if ( ! empty( $excludeIDs ) ) {
$excludeIDs = array_map( 'absint', $excludeIDs );
}
$this->excluded = $excludeIDs;
}
/**
* Find posts that meet AND logic limitations in the title only
*
* @return array|mixed Applicable Post IDs
* @since 1.8
*/
public function get_post_ids_via_and_in_title() {
global $wpdb;
// find posts where all terms appear in the title
$andTerms = array();
$applicableAndResults = true;
$relevantPostIds = $this->relevant_post_ids;
if ( ! empty( $this->relevant_post_ids ) ) {
$this->relevant_post_ids = array_map( 'absint', $this->relevant_post_ids );
}
if ( ! empty( $this->excluded ) ) {
$this->excluded = array_map( 'absint', $this->excluded );
}
$intermediateIncludeSQL = ( ! empty( $this->relevant_post_ids ) ) ? " AND {$this->db_prefix}index.post_id IN (" . implode( ',', $this->relevant_post_ids ) . ') ' : '';
$intermediateExcludeSQL = ( ! empty( $this->excluded ) ) ? " AND {$this->db_prefix}index.post_id NOT IN (" . implode( ',', $this->excluded ) . ') ' : '';
// grab posts with each term in the title
foreach ( $this->terms as $andTerm ) {
// determine whether we want a term match or stem match
$andTermLower = function_exists( 'mb_strtolower' ) ? mb_strtolower( $andTerm, 'UTF-8' ) : strtolower( $andTerm );
if ( ! isset( $postTypeWeights['options']['stem'] ) || empty( $postTypeWeights['options']['stem'] ) ) {
$relavantTermWhere = $wpdb->prepare( " {$this->db_prefix}terms.term = %s ", $andTermLower );
} else {
$unstemmed = $andTermLower;
$maybeStemmed = apply_filters( 'searchwp_custom_stemmer', $unstemmed );
// if the term was stemmed via the filter use it, else generate our own
$andTerm = ( $unstemmed == $maybeStemmed ) ? $this->stemmer->stem( $andTerm ) : $maybeStemmed;
$relavantTermWhere = $wpdb->prepare( " {$this->db_prefix}terms.stem = %s ", $andTerm );
}
$postsWithTermInTitle = $wpdb->get_col(
"SELECT post_id
FROM {$this->db_prefix}index
LEFT JOIN {$this->db_prefix}terms
ON {$this->db_prefix}index.term = {$this->db_prefix}terms.id
WHERE {$relavantTermWhere}
{$intermediateExcludeSQL}
{$intermediateIncludeSQL}
AND {$this->db_prefix}index.title > 0"
);
if ( ! empty( $postsWithTermInTitle ) ) {
$andTerms[] = $postsWithTermInTitle;
} else {
// since no posts were found with this term in the title, our AND logic fails
$applicableAndResults = false;
break;
}
}
// find the common post IDs across the board
if ( $applicableAndResults ) {
$relevantPostIds = call_user_func_array( 'array_intersect', $andTerms );
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', 'Algorithm AND refinement pass: ' . implode( ', ', $relevantPostIds ) );
}
}
return $relevantPostIds;
}
/**
* Opens the main query SQL
*
* @since 1.8
*/
public function query_open() {
global $wpdb;
$this->sql = "SELECT SQL_CALC_FOUND_ROWS {$wpdb->prefix}posts.ID AS post_id, ";
}
/**
* Generate the SQL that calculates overall weight for a post type for a search term
*
* @since 1.8
*/
public function query_sum_post_type_weights() {
// sum our final weights per post type
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] ) {
$termCounter = 1;
$this->sql .= 'SUM( ';
if ( empty( $postTypeWeights['options']['attribute_to'] ) ) {
/** @noinspection PhpUnusedLocalVariableInspection */
foreach ( $this->terms as $term ) {
$this->sql .= "COALESCE(term{$termCounter}.`{$postType}weight`,0) + ";
$termCounter++;
}
} else {
/** @noinspection PhpUnusedLocalVariableInspection */
foreach ( $this->terms as $term ) {
$this->sql .= "COALESCE(term{$termCounter}.`{$postType}attr`,0) + ";
$termCounter++;
}
}
$this->sql = substr( $this->sql, 0, strlen( $this->sql ) - 2 ); // trim off the extra +
$this->sql .= " ) AS `final{$postType}weight`, ";
}
}
}
/**
* Generate the SQL that calculates the overall weight for a search term
*
* @since 1.8
*/
public function query_sum_final_weight() {
global $wpdb;
// build our final, overall weight
$this->sql .= ' SUM( ';
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] ) {
$termCounter = 1;
if ( empty( $postTypeWeights['options']['attribute_to'] ) ) {
/** @noinspection PhpUnusedLocalVariableInspection */
foreach ( $this->terms as $term ) {
$this->sql .= "COALESCE(term{$termCounter}.`{$postType}weight`,0) + ";
$termCounter++;
}
} else {
/** @noinspection PhpUnusedLocalVariableInspection */
foreach ( $this->terms as $term ) {
$this->sql .= "COALESCE(term{$termCounter}.`{$postType}attr`,0) + ";
$termCounter++;
}
}
}
}
$this->sql = substr( $this->sql, 0, strlen( $this->sql ) - 2 ); // trim off the extra +
$this->sql .= " ) AS finalweight FROM {$wpdb->prefix}posts ";
}
/**
* Check whether parent attribution is used anywhere for the current engine
* This needs to be checked because it has a big effect on how aggressive we can make the overall search query
*
* @since 2.4.1
*
* @return bool Whether attribution is applied anywhere given the current engine settings
*/
function maybe_attribution_anywhere() {
$attribution_post_types = array();
$attributed_post_ids = array();
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if (
isset( $postTypeWeights['enabled'] )
&& true == $postTypeWeights['enabled']
&&
(
(
isset( $postTypeWeights['options']['parent'] )
&& ! empty( $postTypeWeights['options']['parent'] )
)
||
(
isset( $postTypeWeights['options']['attribute'] )
&& ! empty( $postTypeWeights['options']['attribute'] )
)
||
(
isset( $postTypeWeights['options']['attribute_to'] )
&& ! empty( $postTypeWeights['options']['attribute_to'] )
)
)
) {
$attribution_post_types[] = $postType;
if ( isset( $postTypeWeights['options']['attribute_to'] )
&& ! empty( $postTypeWeights['options']['attribute_to'] ) ) {
$attributed_post_ids[] = absint( $postTypeWeights['options']['attribute_to'] );
}
if ( isset( $postTypeWeights['options']['attribute'] )
&& ! empty( $postTypeWeights['options']['attribute'] ) ) {
$attributed_post_ids[] = absint( $postTypeWeights['options']['attribute'] );
}
break;
}
}
return array( 'post_types' => $attribution_post_types, 'post_ids' => $attributed_post_ids );
}
/**
* Generate the SQL that defines post type weight
*
* @since 1.8
*/
public function query_post_type_weight() {
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] && empty( $postTypeWeights['options']['attribute_to'] ) ) {
$this->sql .= ", COALESCE(`{$postType}weight`,0) AS `{$postType}weight` ";
}
}
}
/**
* Generate the SQL that defines attributed post type weight
*
* @since 1.8
*/
public function query_post_type_attributed() {
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] && ! empty( $postTypeWeights['options']['attribute_to'] ) ) {
$attributedTo = absint( $postTypeWeights['options']['attribute_to'] );
// make sure we're not excluding the attributed post id
if ( ! in_array( $attributedTo, $this->excluded ) ) {
$this->sql .= ", COALESCE(`{$postType}attr`,0) as `{$postType}attr` ";
} else {
wp_die( 'Search configuration issue: attribution target is excluded' );
}
}
}
}
/**
* Generate the SQL that totals the post weight totals
*
* @since 1.8
*/
public function query_post_type_weight_total() {
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] && empty( $postTypeWeights['options']['attribute_to'] ) ) {
$this->sql .= " COALESCE(`{$postType}weight`,0) +";
}
}
}
/**
* Generate the SQL that totals the attributed post weight totals
*
* @since 1.8
*/
public function query_post_type_attributed_total() {
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] && ! empty( $postTypeWeights['options']['attribute_to'] ) ) {
$attributedTo = absint( $postTypeWeights['options']['attribute_to'] );
// make sure we're not excluding the attributed post id
if ( ! in_array( $attributedTo, $this->excluded ) ) {
$this->sql .= " COALESCE(`{$postType}attr`,0) +";
}
}
}
}
/**
* Generate the SQL that opens the per-term sub-query
*
* @since 1.8
*/
public function query_open_term() {
global $wpdb;
$this->sql .= 'LEFT JOIN ( ';
// our final query cap
$this->sql .= "SELECT {$wpdb->prefix}posts.ID AS post_id ";
// implement our post type weight column
$this->query_post_type_weight();
// implement our post type attributed weight column
$this->query_post_type_attributed();
$this->sql .= ' , ';
// concatenate our total weight with post type weight
$this->query_post_type_weight_total();
// concatenate our total weight with our attributed weight
$this->query_post_type_attributed_total();
$this->sql = substr( $this->sql, 0, strlen( $this->sql ) - 2 ); // trim off the extra +
$this->sql .= ' AS weight ';
$this->sql .= " FROM {$wpdb->prefix}posts ";
}
/**
* Limit results pool by mime type
*
* @param $mimes array Mime types to include
* @since 1.8
*/
public function query_limit_by_mimes( $mimes ) {
global $wpdb;
$targetedMimes = SWP()->get_mimes_from_settings_ids( $mimes );
if ( empty( $targetedMimes ) ) {
return;
}
if ( is_array( $targetedMimes ) ) {
foreach ( $targetedMimes as $key => $val ) {
$targetedMimes[ $key ] = $wpdb->prepare( '%s', $val );
}
}
// we have an array of keys that match MIME types (not subtypes) that we can limit to by appending this condition
$this->sql_status .= " AND {$wpdb->prefix}posts.post_type = 'attachment' AND {$wpdb->prefix}posts.post_mime_type IN ( " . implode( ',', $targetedMimes ) . ' ) ';
}
/**
* Generate the SQL that totals Custom Field weight
*
* @param $weights array|int Custom Field weights from SearchWP settings
*
* @return string SQL to use in the main query
* @since 1.8
*/
public function query_coalesce_custom_fields( $weights) {
$coalesceCustomFields = '0 +';
$this->meta_count = 0;
if ( isset( $weights ) && is_array( $weights ) && ! empty( $weights ) ) {
// first we'll try to merge any matching weight meta_keys so as to save as many JOINs as possible
$optimized_weights = array();
$like_weights = array();
foreach ( $weights as $post_type_meta_guid => $post_type_custom_field ) {
$custom_field_weight = absint( $post_type_custom_field['weight'] );
$post_type_custom_field_key = $post_type_custom_field['metakey'];
// allow developers to implement LIKE matching on custom field keys
if ( false == strpos( $post_type_custom_field_key, '%' ) ) {
$optimized_weights[ $custom_field_weight ][] = $post_type_custom_field_key;
} else {
$like_weights[] = array(
'metakey' => $post_type_custom_field_key,
'weight' => $custom_field_weight,
);
}
}
$totalCustomFields = count( $optimized_weights ) + count( $like_weights );
for ( $i = 0; $i < $totalCustomFields; $i++ ) {
$coalesceCustomFields .= ' COALESCE(cfweights' . $i . '.cfweight' . $i . ',0) + ';
}
$this->meta_count = $totalCustomFields;
}
$coalesceCustomFields = substr( $coalesceCustomFields, 0, strlen( $coalesceCustomFields ) - 2 );
return $coalesceCustomFields;
}
/**
* Generate the SQL that totals taxonomy weight
*
* @param $weights array|int Taxonomy weights from SearchWP settings
*
* @return string SQL to use in the main query
* @since 1.8
*/
public function query_coalesce_taxonomies( $weights ) {
$coalesceTaxonomies = '0 +';
$this->tax_count = 0;
if ( isset( $weights ) && is_array( $weights ) && ! empty( $weights ) ) {
// first we'll try to merge any matching weight taxonomies so as to save as many JOINs as possible
$optimized_weights = array();
foreach ( $weights as $taxonomy_name => $taxonomy_weight ) {
$taxonomy_weight = absint( $taxonomy_weight );
$optimized_weights[ $taxonomy_weight ][] = $taxonomy_name;
}
$totalTaxonomies = count( $optimized_weights );
for ( $i = 0; $i < $totalTaxonomies; $i++ ) {
$coalesceTaxonomies .= ' COALESCE(taxweights' . $i . '.taxweight' . $i . ',0) + ';
}
$this->tax_count = $totalTaxonomies;
}
$coalesceTaxonomies = substr( $coalesceTaxonomies, 0, strlen( $coalesceTaxonomies ) - 2 );
return $coalesceTaxonomies;
}
/**
* Generate the SQL used to open the per-post type sub-query
*
* @param $args array Arguments for the post type
* @since 1.8
*/
public function query_post_type_open( $args ) {
global $wpdb;
$defaults = array(
'post_type' => 'post',
'post_column' => 'ID',
'title_weight' => function_exists( 'searchwp_get_engine_weight' ) ? searchwp_get_engine_weight( 'title' ) : 20,
'slug_weight' => function_exists( 'searchwp_get_engine_weight' ) ? searchwp_get_engine_weight( 'slug' ) : 10,
'content_weight' => function_exists( 'searchwp_get_engine_weight' ) ? searchwp_get_engine_weight( 'content' ) : 2,
'comment_weight' => function_exists( 'searchwp_get_engine_weight' ) ? searchwp_get_engine_weight( 'comment' ) : 1,
'excerpt_weight' => function_exists( 'searchwp_get_engine_weight' ) ? searchwp_get_engine_weight( 'excerpt' ) : 6,
'custom_fields' => 0,
'taxonomies' => 0,
'attributed_to' => false,
);
// process our arguments
$args = wp_parse_args( $args, $defaults );
if ( ! post_type_exists( $args['post_type'] ) ) {
wp_die( 'Invalid request', 'searchwp' );
}
$post_type = $args['post_type'];
$post_column = $args['post_column'];
if ( ! in_array( $post_column, array( 'post_parent', 'ID' ) ) ) {
$post_column = 'ID';
}
$title_weight = absint( $args['title_weight'] );
$slug_weight = absint( $args['slug_weight'] );
$content_weight = absint( $args['content_weight'] );
$comment_weight = absint( $args['comment_weight'] );
$excerpt_weight = absint( $args['excerpt_weight'] );
$wrap_core_weights = apply_filters( 'searchwp_weight_mods_wrap_core_weights', false );
$core_weight_prefix = $wrap_core_weights ? '(' : '';
$core_weight_suffix = $wrap_core_weights ? ')' : '';
$this->sql .= "
LEFT JOIN (
SELECT {$wpdb->prefix}posts.{$post_column} AS post_id,
{$core_weight_prefix}( SUM( {$this->db_prefix}index.title ) * {$title_weight} ) +
( SUM( {$this->db_prefix}index.slug ) * {$slug_weight} ) +
( SUM( {$this->db_prefix}index.content ) * {$content_weight} ) +
( SUM( {$this->db_prefix}index.comment ) * {$comment_weight} ) +
( SUM( {$this->db_prefix}index.excerpt ) * {$excerpt_weight} ) +
{$args['custom_fields']} + {$args['taxonomies']}{$core_weight_suffix}";
// allow developers to inject their own weight modifications
$this->sql .= apply_filters( 'searchwp_weight_mods', '', array(
'engine' => $this->engine,
) );
// the identifier is different if we're attributing
$this->sql .= ! empty( $args['attributed_to'] ) ? " AS `{$post_type}attr` " : " AS `{$post_type}weight` " ;
$this->sql .= "
FROM {$this->db_prefix}terms
LEFT JOIN {$this->db_prefix}index ON {$this->db_prefix}terms.id = {$this->db_prefix}index.term
LEFT JOIN {$wpdb->prefix}posts ON {$this->db_prefix}index.post_id = {$wpdb->prefix}posts.ID
{$this->sql_join}
";
}
/**
* Generate the SQL that extracts Custom Field weights
*
* @param $postType string The post type
* @param $weights array Custom Field weights from SearchWP Settings
* @since 1.8
*/
public function query_post_type_custom_field_weights( $postType, $weights ) {
global $wpdb;
$i = 0;
// first we'll try to merge any matching weight meta_keys so as to save as many JOINs as possible
$optimized_weights = array();
$like_weights = array();
foreach ( $weights as $post_type_meta_guid => $post_type_custom_field ) {
$custom_field_weight = $post_type_custom_field['weight'];
$post_type_custom_field_key = $post_type_custom_field['metakey'];
if ( false !== strpos( $custom_field_weight, '.' ) ) {
$custom_field_weight = (string) abs( floatval( $custom_field_weight ) );
} else {
$custom_field_weight = (string) absint( $custom_field_weight );
}
// allow developers to implement LIKE matching on custom field keys
if ( false == strpos( $post_type_custom_field_key, '%' ) ) {
$optimized_weights[ $custom_field_weight ][] = $post_type_custom_field_key;
} else {
$like_weights[] = array(
'metakey' => $post_type_custom_field_key,
'weight' => $custom_field_weight,
);
}
}
$column = 'ID';
// our custom fields are now keyed by their weight, allowing us to group Custom Fields with the
// same weight together in the same LEFT JOIN
foreach ( $optimized_weights as $weight_key => $meta_keys_for_weight ) {
$post_meta_clause = '';
if ( ! in_array( 'searchwpcfdefault', str_ireplace( ' ', '', $meta_keys_for_weight ) ) ) {
if ( is_array( $meta_keys_for_weight ) ) {
foreach ( $meta_keys_for_weight as $key => $val ) {
$meta_keys_for_weight[ $key ] = $wpdb->prepare( '%s', $val );
}
}
$post_meta_clause = ' AND ' . $this->db_prefix . 'cf.metakey IN (' . implode( ',', $meta_keys_for_weight ) . ')';
}
$weight_key = floatval( $weight_key );
$this->sql .= "
LEFT JOIN (
SELECT {$wpdb->prefix}posts.{$column} as post_id, ( SUM( COALESCE(`{$this->db_prefix}cf`.`count`, 0) ) * {$weight_key} ) AS cfweight{$i}
FROM {$this->db_prefix}terms
LEFT JOIN {$this->db_prefix}cf ON {$this->db_prefix}terms.id = {$this->db_prefix}cf.term
LEFT JOIN {$wpdb->prefix}posts ON {$this->db_prefix}cf.post_id = {$wpdb->prefix}posts.ID
{$this->sql_join}
WHERE {$this->sql_term_where}
{$this->sql_status}
AND {$wpdb->prefix}posts.post_type = '{$postType}'
{$this->sql_exclude}
{$this->sql_include}
{$post_meta_clause}
{$this->sql_conditions}
GROUP BY post_id
) AS cfweights{$i} ON cfweights{$i}.post_id = {$wpdb->prefix}posts.ID";
$i++;
}
// there also may be LIKE weights, though, so we need to build out that SQL as well
if ( ! empty( $like_weights ) ) {
foreach ( $like_weights as $like_weight ) {
$like_weight['metakey'] = $wpdb->prepare( '%s', $like_weight['metakey'] );
$like_weight['weight'] = floatval( $like_weight['weight'] );
$post_meta_clause = ' AND ' . $this->db_prefix . 'cf.metakey LIKE ' . $like_weight['metakey'];
$this->sql .= "
LEFT JOIN (
SELECT {$wpdb->prefix}posts.{$column} as post_id, ( SUM( COALESCE(`{$this->db_prefix}cf`.`count`, 0) ) * {$like_weight['weight']} ) AS cfweight{$i}
FROM {$this->db_prefix}terms
LEFT JOIN {$this->db_prefix}cf ON {$this->db_prefix}terms.id = {$this->db_prefix}cf.term
LEFT JOIN {$wpdb->prefix}posts ON {$this->db_prefix}cf.post_id = {$wpdb->prefix}posts.ID
{$this->sql_join}
WHERE {$this->sql_term_where}
{$this->sql_status}
AND {$wpdb->prefix}posts.post_type = '{$postType}'
{$this->sql_exclude}
{$this->sql_include}
{$post_meta_clause}
{$this->sql_conditions}
GROUP BY post_id
) AS cfweights{$i} ON cfweights{$i}.post_id = {$wpdb->prefix}posts.ID";
$i++;
}
}
}
/**
* Generate the SQL that extracts taxonomy weights
*
* @param $postType string The post type
* @param $weights array Taxonomy weights from SearchWP Settings
* @since 1.8
*/
public function query_post_type_taxonomy_weights( $postType, $weights) {
global $wpdb;
$i = 0;
// first we'll try to merge any matching weight taxonomies so as to save as many JOINs as possible
$optimized_weights = array();
foreach ( $weights as $taxonomy_name => $taxonomy_weight ) {
$taxonomy_weight = absint( $taxonomy_weight );
$optimized_weights[ $taxonomy_weight ][] = $taxonomy_name;
}
foreach ( $optimized_weights as $postTypeTaxWeight => $postTypeTaxonomies ) {
$postTypeTaxWeight = absint( $postTypeTaxWeight );
if ( is_array( $postTypeTaxonomies ) ) {
foreach ( $postTypeTaxonomies as $key => $val ) {
$postTypeTaxonomies[ $key ] = $wpdb->prepare( '%s', $val );
}
}
$this->sql .= "
LEFT JOIN (
SELECT {$this->db_prefix}tax.post_id, ( SUM( {$this->db_prefix}tax.count ) * {$postTypeTaxWeight} ) AS taxweight{$i}
FROM {$this->db_prefix}terms
LEFT JOIN {$this->db_prefix}tax ON {$this->db_prefix}terms.id = {$this->db_prefix}tax.term
LEFT JOIN {$wpdb->prefix}posts ON {$this->db_prefix}tax.post_id = {$wpdb->prefix}posts.ID
{$this->sql_join}
WHERE {$this->sql_term_where}
{$this->sql_status}
AND {$wpdb->prefix}posts.post_type = '{$postType}'
{$this->sql_exclude}
{$this->sql_include}
AND {$this->db_prefix}tax.taxonomy IN (" . implode( ',', $postTypeTaxonomies ) . ")
{$this->sql_conditions}
GROUP BY {$this->db_prefix}tax.post_id
) AS taxweights{$i} ON taxweights{$i}.post_id = {$wpdb->prefix}posts.ID";
$i++;
}
}
/**
* Generate the SQL that closes the per-post type sub-query
*
* @param string $postType The post type
* @param bool|int $attribute_to The attribution target post ID (if applicable)
*
* @since 1.8
*/
public function query_post_type_close( $postType, $attribute_to = false ) {
global $wpdb;
if ( ! post_type_exists( $postType ) ) {
wp_die( 'Invalid request', 'searchwp' );
}
$post_type_group_by = apply_filters( 'searchwp_post_type_group_by_clause', array( "{$wpdb->prefix}posts.ID" ) );
$post_type_group_by = array_map( 'esc_sql', $post_type_group_by );
$post_type_group_by = implode( ', ', $post_type_group_by );
// cap off each enabled post type subquery
$this->sql .= "
WHERE {$this->sql_term_where}
{$this->sql_status}
AND {$wpdb->prefix}posts.post_type = '{$postType}'
{$this->sql_exclude}
{$this->sql_include}
{$this->sql_conditions}
GROUP BY {$post_type_group_by}";
// @since 2.9.0
$this->sql .= $this->only_full_group_by_fix_for_post_type();
if ( isset( $attribute_to ) && ! empty( $attribute_to ) ) {
// $attributedTo was defined in the initial conditional
$attributedTo = absint( $attribute_to );
$this->sql .= ") `attributed{$postType}` ON $attributedTo = {$wpdb->prefix}posts.ID";
} else {
$this->sql .= ") AS `{$postType}weights` ON `{$postType}weights`.post_id = {$wpdb->prefix}posts.ID";
}
}
/**
* MySQL 5.7 has sql_mode=only_full_group_by on by default so we need to accommodate
* our various COALESCE columns by adding to the GROUP BY clause to satisfy the mode
*
* This is run for each post type within each term
*/
public function only_full_group_by_fix_for_post_type() {
if ( empty( $this->tax_count ) && empty( $this->meta_count ) ) {
return '';
}
$taxonomies = array();
$custom_fields = array();
for ( $i = 0; $i < $this->tax_count; $i++ ) {
$taxonomies[] = 'taxweights' . $i . '.taxweight' . $i;
}
$meta = array();
for ( $i = 0; $i < $this->meta_count; $i++ ) {
$custom_fields[] = 'cfweights' . $i . '.cfweight' . $i;
}
return ',' . implode( ',', array_merge( $taxonomies, $custom_fields ) );
}
/**
* Generate the SQL that limits search results to a specific minimum weight per post type
*
* @since 1.8
*/
public function query_limit_post_type_to_weight() {
$this->sql .= ' WHERE ';
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] && empty( $postTypeWeights['options']['attribute_to'] ) ) {
$this->sql .= " COALESCE(`{$postType}weight`,0) +";
}
}
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] && ! empty( $postTypeWeights['options']['attribute_to'] ) ) {
$attributedTo = absint( $postTypeWeights['options']['attribute_to'] );
// make sure we're not excluding the attributed post id
if ( ! in_array( $attributedTo, $this->excluded ) ) {
$this->sql .= " COALESCE(`{$postType}attr`,0) +";
}
}
}
$this->sql = substr( $this->sql, 0, strlen( $this->sql ) - 2 ); // trim off the extra +
$this->sql .= ' > ' . absint( apply_filters( 'searchwp_weight_threshold', 0 ) ) . ' ';
}
/**
* Generate the SQL that limits search results to a specific minimum weight overall
*
* @since 1.8
*/
public function query_limit_to_weight() {
$this->sql .= ' WHERE ';
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] ) {
$termCounter = 1;
if ( empty( $postTypeWeights['options']['attribute_to'] ) ) {
/** @noinspection PhpUnusedLocalVariableInspection */
foreach ( $this->terms as $term ) {
$this->sql .= "COALESCE(term{$termCounter}.`{$postType}weight`,0) + ";
$termCounter++;
}
} else {
/** @noinspection PhpUnusedLocalVariableInspection */
foreach ( $this->terms as $term ) {
$this->sql .= "COALESCE(term{$termCounter}.`{$postType}attr`,0) + ";
$termCounter++;
}
}
}
}
$this->sql = substr( $this->sql, 0, strlen( $this->sql ) - 2 ); // trim off the extra +
$this->sql .= ' > ' . absint( apply_filters( 'searchwp_weight_threshold', 0 ) ) . ' ';
}
function get_collate_override() {
$collate_override = apply_filters(
'searchwp_query_collate_override',
'',
array(
'engine' => $this->engine,
'terms' => implode( ' ', $this->terms )
)
);
// Brute force sanitization.
if ( ! empty( $collate_override ) && is_string( $collate_override ) ) {
$collate_override = ' COLLATE ' . str_replace( '-', '_', sanitize_title_with_dashes( $collate_override ) );
} else {
$collate_override = '';
}
return $collate_override;
}
public function get_enabled_content_types_for_post_type( $post_type, $engine = 'default' ) {
if ( ! SWP()->is_valid_engine( $engine ) ) {
return array();
}
if ( ! post_type_exists( $post_type ) ) {
return array();
}
$engine_settings = SWP()->settings['engines'][ $engine ];
if ( empty( $engine_settings[ $post_type ]['enabled'] ) ) {
return array();
}
$check_native_types = array();
$check_taxonomies = array();
$check_meta_keys = array();
$post_type_weights = $engine_settings[ $post_type ]['weights'];
if ( ! empty( $post_type_weights['title'] ) ) { $check_native_types[] = 'post_title'; }
if ( ! empty( $post_type_weights['content'] ) ) { $check_native_types[] = 'post_content'; }
if ( ! empty( $post_type_weights['excerpt'] ) ) { $check_native_types[] = 'post_excerpt'; }
if ( ! empty( $post_type_weights['slug'] ) ) { $check_native_types[] = 'post_name'; }
if ( ! empty( $post_type_weights['comment'] ) ) { $check_native_types[] = 'comment'; }
if ( isset( $post_type_weights['tax'] ) && is_array( $post_type_weights['tax'] ) && count( $post_type_weights['tax'] ) ) {
foreach ( $post_type_weights['tax'] as $tax => $weight ) {
if ( empty( $weight ) || in_array( $tax, $check_taxonomies ) ) {
continue;
}
$check_taxonomies[] = $tax;
}
}
$persist_extra_metadata = apply_filters( 'searchwp_persist_extra_metadata', false );
// If we're persisting meta keys we need to compare Custom Fields from the engine configuration
// to what actually appears in the database, so we can determine whether we're working with
// a persisted meta key or an actual one.
$post_type_meta_keys_actual = $persist_extra_metadata
? searchwp_get_db_meta_keys_for_post_type( $post_type )
: array();
if ( isset( $post_type_weights['cf'] ) && is_array( $post_type_weights['cf'] ) && count( $post_type_weights['cf'] ) ) {
foreach ( $post_type_weights['cf'] as $custom_field ) {
if ( empty( $custom_field['weight'] ) || in_array( $custom_field['metakey'], $check_meta_keys ) ) {
continue;
}
// Is this a persisted extra metadata key?
if (
$persist_extra_metadata
&& ! in_array( $custom_field['metakey'], $post_type_meta_keys_actual )
) {
$check_meta_keys[] = '_' . SEARCHWP_PREFIX . 'extra_metadata_' . $custom_field['metakey'];
} else {
// It's a native Custom Field, add
$check_meta_keys[] = $custom_field['metakey'];
}
}
}
return array(
'native' => $check_native_types,
'taxonomy' => $check_taxonomies,
'postmeta' => $check_meta_keys,
);
}
public function get_post_ids_for_quoted_search( $query, $engine = 'default' ) {
global $wpdb;
if ( empty( $query ) ) {
$query = str_replace( array( '”', '“' ), '"', SWP()->original_query ); // Accommodate curly quotes.
}
if ( false === strpos( $query, '"' ) ) {
return array();
}
if ( ! SWP()->is_valid_engine( $engine ) ) {
return array();
}
/**
* PHASE 1: Determine the phrase(s) we're matching against.
*/
$phrases = searchwp_get_phrases_from_query( $query );
if ( empty( $phrases ) ) {
return array();
}
/**
* PHASE 2: We need to retrieve the applicable content types from the submitted engine.
*/
$applicable_content_types = array();
$engine_settings = SWP()->settings['engines'][ $engine ];
foreach ( $engine_settings as $engine_post_type => $post_type_settings ) {
if ( ! post_type_exists( $engine_post_type ) || empty( $post_type_settings['enabled'] ) ) {
continue;
}
$applicable_content_types[ $engine_post_type ] = $this->get_enabled_content_types_for_post_type( $engine_post_type, $engine );
}
if ( empty( $applicable_content_types ) ) {
return array();
}
/**
* PHASE 3: Retreive IDs of posts that match our criteria (phrases and config)
*/
$meta_phrase_matches = array();
$taxonomy_phrase_matches = array();
$phrase_search_queries = array();
$phrase_search_arguments = array();
foreach( $phrases as $phrase ) {
foreach( $applicable_content_types as $post_type => $content_types ) {
$meta_phrase_matches = array_merge(
$meta_phrase_matches,
$this->get_phrase_results_from_postmeta(
$post_type,
$content_types,
$phrase
)
);
$taxonomy_phrase_matches = array_merge(
$taxonomy_phrase_matches,
$this->get_phrase_results_from_taxonomies(
$post_type,
$content_types,
$phrase
)
);
// Retrieve the query for native content types for this post type.
$native_query = $this->get_phrase_query_for_native_content_type(
$post_type,
$content_types,
$phrase
);
$phrase_search_queries[] = $native_query['query'];
$phrase_search_arguments = array_merge(
$phrase_search_arguments,
$native_query['arguments']
);
}
}
$taxonomy_phrase_matches_sql = '';
if ( ! empty( $taxonomy_phrase_matches ) ) {
$taxonomy_phrase_matches = array_values( $taxonomy_phrase_matches );
$taxonomy_phrase_matches = array_map( 'absint', $taxonomy_phrase_matches );
$taxonomy_phrase_matches = array_unique( $taxonomy_phrase_matches );
$taxonomy_phrase_matches_sql = ' ID IN (' . implode( ',', $taxonomy_phrase_matches ) . ') OR ';
}
$meta_phrase_matches_sql = '';
if ( ! empty( $meta_phrase_matches ) ) {
$meta_phrase_matches = array_values( $meta_phrase_matches );
$meta_phrase_matches = array_map( 'absint', $meta_phrase_matches );
$meta_phrase_matches = array_unique( $meta_phrase_matches );
$meta_phrase_matches_sql = ' ID IN (' . implode( ',', $meta_phrase_matches ) . ') OR ';
}
$results = $wpdb->get_col(
$wpdb->prepare(
"SELECT DISTINCT ID FROM {$wpdb->posts} WHERE $taxonomy_phrase_matches_sql $meta_phrase_matches_sql " . implode( ' OR ', $phrase_search_queries ),
$phrase_search_arguments
)
);
// NOTE: Attribution is accounted for in the main query.
if ( empty( $results ) ) {
$results = array( 0 );
}
return $results;
}
public function get_phrase_results_from_postmeta( $post_type, $content_types, $phrase ) {
if ( ! array_key_exists( 'postmeta', $content_types ) ) {
return array();
}
if ( empty( $content_types['postmeta'] ) ) {
return array();
}
$like_meta_keys = array();
if (
in_array( 'searchwpcfdefault', $content_types['postmeta'] )
|| in_array( 'searchwp cf default', $content_types['postmeta'] )
) {
$meta_query = array(
array(
'value' => $phrase,
'compare' => 'LIKE',
)
);
} else {
$meta_query = array( 'relation' => 'OR' );
foreach ( $content_types['postmeta'] as $meta_key ) {
// If it's a partial match we need to flag it for additional processing.
if ( false !== strpos( $meta_key, '%' ) ) {
$meta_key = str_ireplace( '%', '{searchwplikemetaplaceholder}', $meta_key);
$like_meta_keys[] = $meta_key;
}
$meta_query[] = array(
'key' => $meta_key,
'value' => $phrase,
'compare' => 'LIKE',
);
}
}
$args = array(
'suppress_filters' => false,
'nopaging' => true,
'post_type' => $post_type,
'post_status' => $post_type == 'attachment' ? array( 'inherit' ) : $this->post_statuses,
'fields' => 'ids',
'meta_query' => $meta_query,
);
if ( ! empty( $like_meta_keys ) ) {
$args['like_meta_keys'] = $like_meta_keys;
}
add_filter( 'posts_where', array( $this, 'like_meta_keys' ), 9908, 2 );
$results = get_posts( $args );
remove_filter( 'posts_where', array( $this, 'like_meta_keys' ), 9908, 2 );
return $results;
}
function like_meta_keys( $where, WP_Query $q ) {
global $wpdb;
$like_meta_keys = $q->get( 'like_meta_keys' );
if ( empty( $like_meta_keys ) ) {
return $where;
}
// Fix all of our LIKE meta key placements.
foreach ( $like_meta_keys as $like_meta_key ) {
$like_meta_key = $wpdb->prepare( '%s', $like_meta_key );
$repaired = str_ireplace( '{searchwplikemetaplaceholder}', '%', $like_meta_key );
$repaired = $wpdb->prepare( '%s', $repaired );
$where = str_replace( "= {$like_meta_key}", "LIKE $repaired", $where );
}
return $where;
}
public function get_phrase_results_from_taxonomies( $post_type, $content_types, $phrase ) {
if ( ! array_key_exists( 'taxonomy', $content_types ) ) {
return array();
}
if ( empty( $content_types['taxonomy'] ) ) {
return array();
}
$tax_query = array();
$args = array(
'name__like' => $phrase,
'fields' => 'ids',
);
foreach ( $content_types['taxonomy'] as $taxonomy ) {
// Find all taxonomy terms that match.
$terms = get_terms( $taxonomy, $args );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
continue;
}
$tax_query[] = array(
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => $terms,
);
}
if ( empty( $tax_query ) ) {
return array();
}
if ( count( $tax_query ) > 1 ) {
$tax_query['relation'] = 'OR';
}
// Find all posts that have this/these terms.
$results = get_posts( array(
'nopaging' => true,
'post_type' => $post_type,
'post_status' => $post_type == 'attachment' ? array( 'inherit' ) : $this->post_statuses,
'fields' => 'ids',
'tax_query' => $tax_query,
) );
return $results;
}
public function get_phrase_query_for_native_content_type( $post_type, $content_types, $phrase ) {
global $wpdb;
if ( ! array_key_exists( 'native', $content_types ) ) {
return '';
}
$phrase = '%' . $wpdb->esc_like( $phrase ) . '%';
$clauses = array();
$arguments = array( $post_type );
// If it's an attachment we need to force 'inherit'.
$post_statuses = $post_type == 'attachment' ? array( 'inherit' ) : $this->post_statuses;
$arguments = array_merge( $arguments, $post_statuses );
$placeholders = array();
foreach ( $post_statuses as $post_status ) {
$placeholders[] = '%s';
}
$sql = "( post_type = %s AND post_status IN (" . implode(',', $placeholders ) . ") AND ";
foreach ( $content_types['native'] as $content_type ) {
// Comments aren't applicable here.
if ( 'comment' === $content_type ) {
continue;
}
// $content_type is hard coded in @get_enabled_content_types_for_post_type.
$arguments[] = $phrase;
$clauses[] = "({$content_type} LIKE %s)";
}
if ( in_array( 'comment', $content_types ) ) {
// TODO: query for match in comment fields
// TODO: consider author name? email?
}
$sql .= '(' . implode( ' OR ', $clauses ) . '))';
return array(
'query' => $sql,
'arguments' => $arguments,
);
}
/**
* Dynamically generate SQL query based on engine settings and retrieve a weighted, ordered list of posts
*
* @return bool|array Post IDs found in the index
* @since 1.0
*/
function query_for_post_ids() {
global $wpdb;
do_action( 'searchwp_log', 'query_for_post_ids()' );
// check to make sure there are settings for the current engine
if ( ! isset( $this->settings['engines'][ $this->engine ] ) && is_array( $this->settings['engines'][ $this->engine ] ) ) {
return false;
}
// check to make sure we actually have terms to search
// TODO: refactor this
if ( empty( $this->terms ) ) {
// short circuit
$this->foundPosts = 0;
$this->maxNumPages = 0;
$this->postIDs = array();
do_action( 'searchwp_log', 'No terms, short circuit' );
return false;
}
// check to make sure that all post types in the settings are still in fact registered and active
// e.g. in case a Custom Post Type was saved in the settings but no longer exists
$this->validate_post_types();
// we might need to short circuit for a number of reasons
if ( ! $this->any_enabled_post_types() ) {
do_action( 'searchwp_log', 'No enabled post types, short circuit' );
return false;
}
// allow devs to filter excluded IDs
$this->excluded = apply_filters( 'searchwp_exclude', $this->excluded, $this->engine, $this->terms );
if ( is_array( $this->excluded ) ) {
$this->excluded = array_map( 'absint', $this->excluded );
}
// perform our AND logic before getting started
// e.g. we're going to limit to posts that have all of the search terms
$this->maybe_do_and_logic();
$this->exclude_posts_by_weight();
// Build exclusion SQL
$this->sql_exclude = ( ! empty( $this->excluded ) ) ? " AND {$wpdb->prefix}posts.ID NOT IN (" . implode( ',', $this->excluded ) . ') ' : '';
// if there's an insane number of posts returned, we're dealing with a site with a lot of similar content
// so we need to trim out the initial results by relevance before proceeding else we'll have a wicked slow query
// NOTE: this only applies if titles have weights for all enabled post types, so we must check that first
$able_to_refine_results = true;
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] ) {
$title_weight = isset( $postTypeWeights['weights']['title'] ) ? absint( $postTypeWeights['weights']['title'] ) : 0;
if ( 0 == $title_weight ) {
// at least one title weight is zero so we are NOT ABLE to refine results any
// further because the post IDs we find when refining by title will not apply
// in the main search query since those title hits are worth nothing
$able_to_refine_results = false;
do_action( 'searchwp_log', 'Unable to further refine results' );
break;
}
}
}
// Allow for quoted search restrictions.
$do_quoted_searches = apply_filters( 'searchwp_allow_quoted_phrase_search', false );
$phrase_query = str_replace( array( '”', '“' ), '"', SWP()->original_query ); // Accommodate curly quotes.
if ( false !== strpos( $phrase_query, '"' ) && $do_quoted_searches ) {
$this->included = $this->get_post_ids_for_quoted_search( $phrase_query, $this->engine );
if ( empty( $this->included ) || $this->included === array( 0 ) ) {
// If a quoted search returns no results we will fall back to quote-less search by unsetting the $this->included limiter
// which will in turn trigger the original functionality of SearchWP which is to remove all punctuation.
$revert_to_no_quotes = apply_filters( 'searchwp_quoted_search_automatic_fallback', true );
if ( $revert_to_no_quotes ) {
$this->included = array();
$exact_match_args = array( 'type' => 'exact-match' );
if (
apply_filters( 'searchwp_auto_output_revised_search_query', true, $exact_match_args )
) {
add_action( 'loop_start', 'searchwp_output_revised_search_query' );
}
do_action( 'searchwp_revised_search_query', $exact_match_args );
}
}
}
// There's a limitation when it comes to AND logic refining that comes into play
// if weight transfer comes into play. So our default will be true unless there is
// some sort of attribution taking place for this engine.
$refine_and_results_default = true;
foreach ( $this->settings['engines'][ $this->engine ] as $post_type => $post_type_settings ) {
if (
(
isset( $post_type_settings['options']['attribute_to'] )
&& ! empty( $post_type_settings['options']['attribute_to'] )
)
||
(
isset( $post_type_settings['options']['parent'] )
&& ! empty( $post_type_settings['options']['parent'] )
)
) {
$refine_and_results_default = false;
break;
}
}
// if the include pool has not been limited, do that
if ( empty( $this->included ) ) {
$parity = count( $this->terms );
$maxNumAndResults = absint( apply_filters( 'searchwp_max_and_results', 300 ) );
if (
$parity > 1
&& $able_to_refine_results
&& apply_filters( 'searchwp_refine_and_results', $refine_and_results_default )
&& count( $this->relevant_post_ids ) > $maxNumAndResults
) {
$this->relevant_post_ids = $this->get_post_ids_via_and_in_title();
do_action( 'searchwp_log', 'Refining AND results based on Title' );
}
// make sure we've got an array of unique integers
$this->relevant_post_ids = array_map( 'absint', array_unique( $this->relevant_post_ids ) );
} else {
$this->relevant_post_ids = $this->included;
}
// allow devs to filter included post IDs
add_filter( 'searchwp_force_wp_query', '__return_true' );
$this->included = apply_filters( 'searchwp_include', $this->relevant_post_ids, $this->engine, $this->terms );
remove_filter( 'searchwp_force_wp_query', '__return_true' );
// allow devs to force AND logic all the time, no matter what (if there was more than one search term)
$forceAnd = ( count( $this->terms ) > 1 && apply_filters( 'searchwp_and_logic_only', false ) ) ? true : false;
// if it was totally empty and AND logic is forced, we'll hit a SQL error, so populate it with an impossible ID
if ( empty( $this->included ) && $forceAnd ) {
$this->included = array( 0 );
}
if ( is_array( $this->included ) ) {
$this->included = array_map( 'absint', $this->included );
}
$this->sql_include = ( ( is_array( $this->included ) && ! empty( $this->included ) ) || $forceAnd ) ? " AND {$wpdb->prefix}posts.ID IN (" . implode( ',', $this->included ) . ') ' : '';
/**
* Build the search query
*/
$this->query_open();
// allow for injection into main SELECT
$select_inject = trim( (string) apply_filters( 'searchwp_query_select_inject', '' ) );
if ( ! empty( $select_inject ) ) {
// we're automatically going to append the comma, so if it was returned we can kill it
if ( ',' == substr( $select_inject, -1 ) ) {
$select_inject = substr( $select_inject, 0, strlen( $select_inject ) - 1 );
}
$this->sql .= ' ' . $select_inject . ' , ';
}
$this->query_sum_post_type_weights();
$this->query_sum_final_weight();
// allow for pre-algorithm join
$this->sql = ' ' . (string) apply_filters( 'searchwp_query_main_join', $this->sql, $this->engine ) . ' ';
// loop through each submitted term
$termCounter = 1;
foreach ( $this->terms as $term ) {
$this->query_open_term();
// build our post type queries
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] ) {
// TODO: store our post format clause and integrate
// TODO: store our post status clause and integrate
// prep the term for this combination of term and post type config
$prepped_term = $this->prep_term( $term, $postTypeWeights );
$term = $prepped_term['term'];
$term_or_stem = $prepped_term['term_or_stem'];
$original_prepped_term = $prepped_term['original_prepped_term'];
$this->cache_term_final( $term );
// build our final term WHERE
if ( ! in_array( $term_or_stem, array( 'term', 'stem' ) ) ) {
wp_die( 'Invalid request', 'searchwp' );
}
$collate_override = $this->get_collate_override();
$this->sql_term_where = " {$this->db_prefix}terms." . $term_or_stem . $collate_override . ' IN (' . implode( ',', $term ) . ')';
/** @noinspection PhpUnusedLocalVariableInspection */
$last_term = $term;
// if it's an attachment we need to force 'inherit'
$post_statuses = $postType == 'attachment' ? array( 'inherit' ) : $this->post_statuses;
if ( is_array( $post_statuses ) ) {
foreach ( $post_statuses as $key => $val ) {
$post_statuses[ $key ] = $wpdb->prepare( '%s', $val );
}
}
$this->sql_status = "AND {$wpdb->prefix}posts.post_status IN ( " . implode( ',', $post_statuses ) . ' ) ';
// determine whether we need to limit to a mime type
if ( isset( $postTypeWeights['options']['mimes'] ) && '' !== $postTypeWeights['options']['mimes'] ) {
// stored as an array of integers that correlate to mime type groups
$mimes = explode( ',', $postTypeWeights['options']['mimes'] );
$mimes = array_map( 'absint', $mimes );
$this->query_limit_by_mimes( $mimes );
}
// Take into consideration the engine limiter rules FOR THIS POST TYPE
$limited_ids = $this->get_included_ids_from_taxonomies_for_post_type( $postType );
$limiter_column = 'ID';
// If parent attribution is in play we need to transfer the inclusion/exclusion rules
if (
'attachment' === $postType
&& isset( $postTypeWeights['options']['parent'] )
&& ! empty( $postTypeWeights['options']['parent'] )
) {
$limiter_column = 'post_parent';
$global_limited_ids = array();
// This isn't ideal because the post_parent can be _any_ post type, so we need to limit to them all...
foreach ( $this->engineSettings as $limiter_post_type => $limiter_post_type_weights ) {
if ( ! isset( $limiter_post_type_weights['enabled'] ) || empty( $limiter_post_type_weights['enabled'] ) ) {
continue;
}
$these_limited_ids = $this->get_included_ids_from_taxonomies_for_post_type( $limiter_post_type );
if ( ! empty( $these_limited_ids ) ) {
$global_limited_ids = array_merge( $global_limited_ids, $these_limited_ids );
}
}
$limited_ids = array_unique( $global_limited_ids );
}
// Function returns false if not applicable
if ( is_array( $limited_ids ) && ! empty( $limited_ids ) ) {
$limited_ids = array_map( 'absint', $limited_ids );
$limited_ids = array_unique( $limited_ids );
$this->sql_status .= " AND {$wpdb->prefix}posts.post_type = '{$postType}' AND {$wpdb->prefix}posts." . $limiter_column . " IN ( " . implode( ',', $limited_ids ) . ' ) ';
}
// reset back to our original term
$term = $original_prepped_term;
// we need to use absint because if a weight was set to -1 for exclusion, it was already forcefully excluded
$titleWeight = isset( $postTypeWeights['weights']['title'] ) ? absint( $postTypeWeights['weights']['title'] ) : 0;
$slugWeight = isset( $postTypeWeights['weights']['slug'] ) ? absint( $postTypeWeights['weights']['slug'] ) : 0;
$contentWeight = isset( $postTypeWeights['weights']['content'] ) ? absint( $postTypeWeights['weights']['content'] ) : 0;
$excerptWeight = isset( $postTypeWeights['weights']['excerpt'] ) ? absint( $postTypeWeights['weights']['excerpt'] ) : 0;
if ( apply_filters( 'searchwp_index_comments', true ) ) {
$commentWeight = isset( $postTypeWeights['weights']['comment'] ) ? absint( $postTypeWeights['weights']['comment'] ) : 0;
} else {
$commentWeight = 0;
}
// build the SQL to accommodate Custom Fields
$custom_field_weights = isset( $postTypeWeights['weights']['cf'] ) ? $postTypeWeights['weights']['cf'] : 0;
$coalesceCustomFields = $this->query_coalesce_custom_fields( $custom_field_weights );
// build the SQL to accommodate Taxonomies
$taxonomy_weights = isset( $postTypeWeights['weights']['tax'] ) ? $postTypeWeights['weights']['tax'] : 0;
$coalesceTaxonomies = $this->query_coalesce_taxonomies( $taxonomy_weights );
// allow additional tables to be joined
$this->sql_join = apply_filters( 'searchwp_query_join', '', $postType, $this->engine );
if ( ! is_string( $this->sql_join ) ) {
$this->sql_join = '';
}
// allow additional conditions
$this->sql_conditions = apply_filters( 'searchwp_query_conditions', '', $postType, $this->engine );
if ( ! is_string( $this->sql_conditions ) ) {
$this->sql_conditions = '';
}
// if we're dealing with attributed weight we need to make sure that the attribution target was not excluded
$excludedByAttribution = false;
$attributedTo = false;
if ( isset( $postTypeWeights['options']['attribute_to'] ) && ! empty( $postTypeWeights['options']['attribute_to'] ) ) {
$postColumn = 'ID';
$attributedTo = absint( $postTypeWeights['options']['attribute_to'] );
if ( in_array( $attributedTo, $this->excluded ) ) {
$excludedByAttribution = true;
}
} else {
// if it's an attachment and we want to attribute to the parent, we need to set that here
$postColumn = ! empty( $postTypeWeights['options']['parent'] ) ? 'post_parent' : 'ID';
}
// open up the post type subquery if not excluded by attribution
if ( ! $excludedByAttribution ) {
$post_type_params = array(
'post_type' => $postType,
'post_column' => $postColumn,
'title_weight' => $titleWeight,
'slug_weight' => $slugWeight,
'content_weight' => $contentWeight,
'comment_weight' => $commentWeight,
'excerpt_weight' => $excerptWeight,
'custom_fields' => isset( $coalesceCustomFields ) ? $coalesceCustomFields : '',
'taxonomies' => isset( $coalesceTaxonomies ) ? $coalesceTaxonomies : '',
'attributed_to' => $attributedTo,
);
$this->query_post_type_open( $post_type_params );
// handle custom field weights
if ( isset( $postTypeWeights['weights']['cf'] ) && is_array( $postTypeWeights['weights']['cf'] ) && ! empty( $postTypeWeights['weights']['cf'] ) ) {
$this->query_post_type_custom_field_weights( $postType, $postTypeWeights['weights']['cf'] );
}
// handle taxonomy weights
if ( isset( $postTypeWeights['weights']['tax'] ) && is_array( $postTypeWeights['weights']['tax'] ) && ! empty( $postTypeWeights['weights']['tax'] ) ) {
$this->query_post_type_taxonomy_weights( $postType, $postTypeWeights['weights']['tax'] );
}
// close out the per-post type sub-query
$attribute_to = isset( $postTypeWeights['options']['attribute_to'] ) ? absint( $postTypeWeights['options']['attribute_to'] ) : false;
$this->query_post_type_close( $postType, $attribute_to );
}
}
}
$this->sql .= " LEFT JOIN {$this->db_prefix}index ON {$this->db_prefix}index.post_id = {$wpdb->prefix}posts.ID ";
$this->sql .= " LEFT JOIN {$this->db_prefix}terms ON {$this->db_prefix}terms.id = {$this->db_prefix}index.term ";
// make sure we're only getting posts with actual weight
$this->query_limit_post_type_to_weight();
$this->sql .= $this->query_limit_pool_by_stem();
$this->sql .= $this->post_status_limiter_sql( $this->engineSettings );
$this->sql .= ' GROUP BY post_id';
$this->sql .= $this->only_full_group_by_fix_for_term();
$this->sql .= " ) AS term{$termCounter} ON term{$termCounter}.post_id = {$wpdb->prefix}posts.ID ";
$termCounter++;
}
/**
* END LOOP THROUGH EACH SUBMITTED TERM
*/
// make sure we're only getting posts with actual weight
$this->query_limit_to_weight();
// There's a potential bug in MariaDB 10.3.20 which causes searches to fail
// bceause the subqueries return NULLed results which in turn cause this
// extra limiter to fail. Further research necessary.
if ( apply_filters( 'searchwp_query_strict_limiters', true ) ) {
$this->sql .= $this->post_status_limiter_sql( $this->engineSettings );
}
$modifier = ( $this->postsPer < 1 ) ? 1 : $this->postsPer; // if posts_per_page is -1 there's no offset
$start = ! empty( $this->offset ) ? $this->offset : intval( ( $this->page - 1 ) * $modifier );
$total = intval( $this->postsPer );
$order = $this->order;
// accommodate a custom offset
$start = absint( apply_filters( 'searchwp_query_limit_start', $start, $this->page, $this->engine, $this ) );
$total = absint( apply_filters( 'searchwp_query_limit_total', $total, $this->page, $this->engine, $this ) );
$extraWhere = apply_filters( 'searchwp_where', '', $this->engine, $this );
$this->sql .= ' ' . $extraWhere . ' ';
// allow developers to order by date
$orderByDate = apply_filters( 'searchwp_return_orderby_date', false, $this->engine, $this );
$finalOrderBySQL = $orderByDate ? " ORDER BY post_date {$order}, finalweight {$order} " : " ORDER BY finalweight {$order}, post_date DESC ";
// allow developers to return completely random results that meet the minumum weight
if ( apply_filters( 'searchwp_return_orderby_random', false, $this->engine, $this ) ) {
$finalOrderBySQL = ' ORDER BY RAND() ';
}
// allow for arbitrary ORDER BY filtration
$finalOrderBySQL = apply_filters( 'searchwp_query_orderby', $finalOrderBySQL, $this->engine, $this );
if ( apply_filters( 'searchwp_query_allow_query_string_override_orderby', true, $this ) ) {
if ( ! empty( $_GET['orderby'] ) ) {
$query_orderby = $this->get_query_string_orderby();
if ( ! empty( $query_orderby ) ) {
$finalOrderBySQL = esc_sql( " ORDER BY {$query_orderby} {$order}" );
}
}
}
// make sure we limit the overall wp_posts pool to what was returned in the subqueries
if ( $forceAnd ) {
for ( $i = 1; $i <= count( $this->terms ); $i++ ) {
$this->sql .= " AND {$wpdb->prefix}posts.ID IN (term" . $i . '.post_id) ';
}
} else {
$end_cap_limiter = '';
for ( $i = 1; $i <= count( $this->terms ); $i++ ) {
$end_cap_limiter .= 'term' . $i . '.post_id,';
}
$this->sql .= " AND {$wpdb->prefix}posts.ID IN (" . substr( $end_cap_limiter, 0, strlen( $end_cap_limiter ) - 1 ) . ') ';
}
// also limit the wp_posts pool taking into consideration exclusions
$this->sql .= $this->sql_exclude;
// group the results
$this->sql .= " GROUP BY post_id ";
// $this->sql .= $this->only_full_group_by_fix_for_query();
$this->sql .= $finalOrderBySQL . ' ';
if ( $this->postsPer > 0 ) {
$this->sql .= " LIMIT {$start}, {$total}";
}
$this->sql = str_replace( "\n", ' ', $this->sql );
$this->sql = str_replace( "\t", ' ', $this->sql );
// allow BIG_SELECTS
$bigSelects = apply_filters( 'searchwp_big_selects', false );
if ( $bigSelects ) {
$wpdb->query( 'SET SQL_BIG_SELECTS=1' );
}
// retrieve all results and associated weights (SQL was prepared throughout generation)
$searchwp_query_results = $wpdb->get_results( $this->sql );
// if there was in fact a SQL_BIG_SELECTS error let's grab it and try the query again
if ( isset ( $wpdb->last_error ) && false !== strpos( $wpdb->last_error, 'SQL_BIG_SELECTS' ) && current_user_can( apply_filters( 'searchwp_settings_cap', 'manage_options' ) ) ) {
do_action( 'searchwp_log', "!!! SQL_BIG_SELECTS error detected, please add_filter( 'searchwp_big_selects', '__return_true' );" );
// show an entry in the admin bar if it's visible
if ( is_admin_bar_showing() ) {
add_action( 'wp_footer', array( $this, 'admin_bar_sql_big_selects_notice_assets' ) );
add_action( 'wp_before_admin_bar_render', array( $this, 'admin_bar_sql_big_selects_notice' ), 999 );
} else {
// TODO: the query failed, so no results are showing, we can't filter titles or content, so we need to do something
}
}
// format the results
$postIDs = array(); // going to store all of the returned post IDs
$this->results_weights = array(); // store all of the specific weights
if ( ! empty( $searchwp_query_results ) ) {
foreach ( $searchwp_query_results as $searchwp_query_result ) {
// store the weights for this post
$weights = array(
'post_id' => null,
'weight' => null,
'post_types' => array()
);
// the results returned are just the table results from the query, let's format them a bit
foreach ( $searchwp_query_result as $searchwp_query_result_key => $searchwp_query_result_value ) {
switch ( $searchwp_query_result_key ) {
case 'post_id' :
$postIDs[] = absint( $searchwp_query_result->post_id );
$weights['post_id'] = absint( $searchwp_query_result->post_id );
break;
case 'finalweight' :
$weights['weight'] = absint( $searchwp_query_result_value );
break;
default :
$weight_key = str_replace( array( 'final', 'weight' ), '', $searchwp_query_result_key );
$weights['post_types'][ $weight_key ] = absint( $searchwp_query_result_value );
break;
}
}
$this->results_weights[ $searchwp_query_result->post_id ] = $weights;
}
}
do_action( 'searchwp_log', 'Search results (' . count( $postIDs ) . ') IDs: ' . implode( ', ', $postIDs ) );
// retrieve how many total posts were found without the limit
$this->foundPosts = (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' );
// store an accurate max_num_pages for $wp_query
$this->maxNumPages = ( $this->postsPer < 1 ) ? 1 : ceil( $this->foundPosts / $this->postsPer );
// store our post IDs
$this->postIDs = $postIDs;
return true;
}
/**
* Related to only_full_group_by_fix_for_post_type() but runs for the whole query
*/
function only_full_group_by_fix_for_query() {
$return = array();
$total_terms = count( $this->terms );
for ( $i = 1; $i <= $total_terms; $i++ ) {
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] ) {
if ( empty( $postTypeWeights['options']['attribute_to'] ) ) {
$return[] = 'term' . $i . '.`' . $postType . 'weight`';
} else {
$return[] = 'term' . $i . '.`' . $postType . 'attr`';
}
}
}
}
return ' ,' . implode( ',', $return ) . ' ';
}
/**
* Use query string to force final orderby
*/
function get_query_string_orderby() {
global $wpdb;
if ( empty( $_GET['orderby'] ) ) {
return '';
}
$this_orderby = strtolower( $_GET['orderby'] );
// Keys are the query string, values are the database column
$allowed_orderbys = array(
'title' => 'post_title'
);
if ( ! array_key_exists( $this_orderby, $allowed_orderbys ) ) {
return '';
}
return $wpdb->posts . '.' . $allowed_orderbys[ $this_orderby ];
}
/**
* Callback when an error was detected during the search, outputs CSS for the Admin bar
*
* @since 2.3.2
*/
function admin_bar_sql_big_selects_notice_assets() {
?><style type="text/css">
#wpadminbar #wp-admin-bar-searchwp-sql-big-selects-notice,
#wpadminbar #wp-admin-bar-searchwp-sql-big-selects-notice > a {
background-color:#c00 !important;
color:#fff !important;
}
</style><?php
}
/**
* Output a notice in the Admin Bar so users can quickly fix issues with known fixes
*
* @since 2.3.2
*/
function admin_bar_sql_big_selects_notice() {
global $wp_admin_bar;
if ( method_exists( $wp_admin_bar, 'add_menu' ) ) {
$args = array(
'id' => 'searchwp-sql-big-selects-notice',
'title' => __( 'SearchWP Error', 'searchwp' ),
'href' => 'https://searchwp.com/docs/hooks/searchwp_big_selects/',
);
$wp_admin_bar->add_menu( $args );
$wp_admin_bar->add_menu( array(
'parent' => 'searchwp-sql-big-selects-notice',
'id' => 'searchwp-sql-big-selects-notice-sub',
'title' => __( 'View SQL_BIG_SELECTS Fix', 'searchwp' ),
'href' => 'https://searchwp.com/docs/hooks/searchwp_big_selects/',
) );
}
}
/**
* Cache the final term(s) after filtering to prevent redundant queries
*
* @param $term
*
* @since 2.3
*/
function cache_term_final( $term ) {
// $term has been prepared already
$this->terms_final = array_merge( $this->terms_final, $term );
$this->terms_final = array_filter( $this->terms_final, 'strlen' );
$this->terms_final = array_unique( $this->terms_final );
}
/**
* @param $term
*
* @param $postTypeWeights
*
* @return array
*/
function prep_term( $term, $postTypeWeights ) {
global $wpdb;
$original_prepped_term = $term;
$term = function_exists( 'mb_strtolower' ) ? mb_strtolower( $term, 'UTF-8' ) : strtolower( $term );
// determine whether we're stemming or not
$term_or_stem = 'term';
if ( isset( $postTypeWeights['options']['stem'] ) && ! empty( $postTypeWeights['options']['stem'] ) ) {
// build our stem
$term_or_stem = 'stem';
$unstemmed = $term;
$maybeStemmed = apply_filters( 'searchwp_custom_stemmer', $unstemmed );
// if the term was stemmed via the filter use it, else generate our own
$term = ( $unstemmed == $maybeStemmed ) ? $this->stemmer->stem( $term ) : $maybeStemmed;
// It's only a valid stem if the original term is in fact in the index, so let's verify.
$validate_stem = apply_filters( 'searchwp_stem_validate', true );
if ( $validate_stem ) {
$in_index = $wpdb->query(
$wpdb->prepare(
"
SELECT * FROM {$this->db_prefix}terms
WHERE stem = %s LIMIT 1
",
$term
)
);
// If it's an invalid stem then we need to revert back to the original, prepped term for additional processing (e.g. partial matches)
if ( empty( $in_index ) ) {
$term = $original_prepped_term;
}
}
}
// set up our term operator (e.g. LIKE terms or fuzzy matching)
// since we're going to allow extending the term WHERE SQL, we need to force $term as an array
// because in many cases with extensions it will be
$term = array( $term );
// let extensions filter this all day
$term = apply_filters( 'searchwp_term_in', $term, $this->engine, $original_prepped_term );
// prepare our terms
if ( ! is_array( $term ) ) {
$term = explode( ' ', $term );
}
if ( empty( $term ) ) {
// if it got messed with so bad it's no longer an array, we're going to revert
$term = array( $original_prepped_term );
}
$term = array_unique( $term );
// hopefully the developer sanitized their terms, but they might have prepared them (i.e. they're wrapped in single quotes)
foreach ( $term as $raw_term_key => $raw_term ) {
if ( "'" == substr( $raw_term, 0, 1 ) && "'" == substr( $raw_term, strlen( $raw_term ) - 1 ) ) {
$raw_term = substr( $raw_term, 1, strlen( $raw_term ) - 2 );
}
$raw_term = trim( sanitize_text_field( $raw_term ) );
$raw_term_prepared = $wpdb->prepare( '%s', $raw_term );
$raw_term_lower = function_exists( 'mb_strtolower' ) ? mb_strtolower( $raw_term_prepared, 'UTF-8' ) : strtolower( $raw_term_prepared );
$term[ $raw_term_key ] = $raw_term_lower;
}
return array( 'term' => $term, 'term_or_stem' => $term_or_stem, 'original_prepped_term' => $original_prepped_term );
}
/**
* Related to only_full_group_by_fix_for_post_type() but runs for each term in the query
* and includes the COALESCED columns for
*/
public function only_full_group_by_fix_for_term() {
$post_types = array();
foreach ( $this->engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] ) {
if ( empty( $postTypeWeights['options']['attribute_to'] ) ) {
$post_types[] = '`' . $postType . 'weights`.`' . $postType . 'weight`';
} else {
$post_types[] = '`attributed' . $postType . '`.`' . $postType . 'attr`';
}
}
}
if ( empty( $post_types ) ) {
return '';
}
return ' ,' . implode( ',', $post_types ) . ' ';
}
/**
* Apply a limiter based on the term stem(s)
*
* @internal param $terms
*
* @return string
*
* @since 2.3
*/
function query_limit_pool_by_stem() {
global $wpdb;
$sql = '';
// limit the full pool to search term(s) stem
if ( is_array( $this->terms_final ) && count( $this->terms_final ) ) {
$collate_override = $this->get_collate_override();
// if stemming was enabled, the terms have already been stemmed
$limiter_sql = " ( {$this->db_prefix}terms.term " . $collate_override . " IN (" . implode( ',', $this->terms_final ) . ") OR {$this->db_prefix}terms.stem " . $collate_override . " IN (" . implode( ',', $this->terms_final ) . ') ) ';
// if attribution is concerned, the post_parent likely WILL NOT have the term or stem, so we need to accommodate
// by adding a conditional that excuses attributed post types that do not have any terms/stems
$post_types_with_attribution = $this->maybe_attribution_anywhere();
if ( is_array( $post_types_with_attribution['post_types'] ) ) {
foreach ( $post_types_with_attribution['post_types'] as $key => $val ) {
$post_types_with_attribution['post_types'][ $key ] = $wpdb->prepare( '%s', $val );
}
}
if ( ! empty( $post_types_with_attribution['post_types'] ) ) {
$limiter_sql .= " OR ( {$wpdb->posts}.post_type NOT IN (" . implode( ',', $post_types_with_attribution['post_types'] ) . ') ) ';
// if we can also allow specific post IDs, do that
if ( count( $post_types_with_attribution['post_ids'] ) ) {
$attributed_post_ids = array_map( 'absint', $post_types_with_attribution['post_ids'] );
$limiter_sql .= " OR {$wpdb->posts}.ID IN (" . implode( ',', $attributed_post_ids ) . ') ';
}
}
// let it rip
$sql .= ' AND ( ' . $limiter_sql . ' ) ';
}
return $sql;
}
/**
* Retrieve post IDs of parents
*
* @since 2.6.1
* @return array
*/
function get_attributed_parent_ids() {
global $wpdb;
// if attribution is concerned, the post_parent likely WILL NOT have the term or stem, so we need to accommodate
// by adding a conditional that excuses attributed post types that do not have any terms/stems
$post_types_with_attribution = $this->maybe_attribution_anywhere();
if ( is_array( $post_types_with_attribution['post_types'] ) ) {
foreach ( $post_types_with_attribution['post_types'] as $key => $val ) {
$post_types_with_attribution['post_types'][ $key ] = $wpdb->prepare( '%s', $val );
}
}
// by default $post_types_with_attribution['post_ids'] stores any specific attribution target IDs
$post_types_with_attribution['post_ids'] = array_map( 'absint', $post_types_with_attribution['post_ids'] );
// if an entire post type needs attribution...
if ( ! empty( $post_types_with_attribution['post_types'] ) ) {
// this has potential to be a performance nightmare because we are essentially looking for *anything* in the
// entire index that is not an attributed post type (because it can't be) and that it also has neither a term
// nor a stem (else it would show up as a result of the main query limiter) so let's try to reduce that pool
// by grabbing the parent IDs of posts that do have the term(s) and using those as attribution IDs
$collate_override = $this->get_collate_override();
// grab post_parent of all attributed post types that DO have the search phrases
$attributed_post_parents_sql = "
SELECT DISTINCT {$wpdb->posts}.post_parent
FROM {$wpdb->posts}
LEFT JOIN {$this->db_prefix}index ON {$this->db_prefix}index.post_id = {$wpdb->posts}.ID
LEFT JOIN {$this->db_prefix}terms ON {$this->db_prefix}terms.id = {$this->db_prefix}index.term
WHERE {$wpdb->posts}.post_parent > 0
AND (
{$this->db_prefix}terms.term " . $collate_override . " IN (" . implode( ',', $this->terms_final ) . ")
OR {$this->db_prefix}terms.stem " . $collate_override . " IN (" . implode( ',', $this->terms_final ) . ")
)
AND {$wpdb->posts}.post_type IN (" . implode( ',', $post_types_with_attribution['post_types'] ) . ')';
$attributed_post_parent_ids = $wpdb->get_col( $attributed_post_parents_sql );
$attributed_post_parent_ids = array_map( 'absint', $attributed_post_parent_ids );
// if we found IDs merge them to any single attributed IDs
if ( count( $attributed_post_parent_ids ) ) {
$post_types_with_attribution['post_ids'] = array_merge( $post_types_with_attribution['post_ids'], $attributed_post_parent_ids );
}
}
// make sure we have all ints
$post_types_with_attribution['post_ids'] = array_map( 'absint', $post_types_with_attribution['post_ids'] );
$post_types_with_attribution['post_ids'] = array_unique( $post_types_with_attribution['post_ids'] );
return $post_types_with_attribution['post_ids'];
}
/**
* Generate the SQL used to limit the results pool as much as possible while considering enabled post types
*
* @param $engineSettings array The engine settings from the SearchWP settings
*
* @return string
*/
public function post_status_limiter_sql( $engineSettings ) {
global $wpdb;
$prefix = $wpdb->prefix;
$sql = '';
// add more limiting
$finalPostTypes = array();
$finalPostTypesIncludesAttachments = false;
foreach ( $engineSettings as $postType => $postTypeWeights ) {
if ( isset( $postTypeWeights['enabled'] ) && true == $postTypeWeights['enabled'] ) {
if ( 'attachment' == $postType ) {
$finalPostTypesIncludesAttachments = true;
} else {
$finalPostTypes[] = $postType;
}
}
}
$sql .= ' AND ( ';
// based on whether attachments are the ONLY enabled post type, we'll build out this statement
if ( ! empty( $finalPostTypes ) ) {
$post_statuses = $this->post_statuses;
if ( is_array( $post_statuses ) ) {
foreach ( $post_statuses as $key => $val ) {
$post_statuses[ $key ] = $wpdb->prepare( '%s', $val );
}
}
if ( is_array( $finalPostTypes ) ) {
foreach ( $finalPostTypes as $key => $val ) {
$finalPostTypes[ $key ] = $wpdb->prepare( '%s', $val );
}
}
$sql .= " ( {$prefix}posts.post_status IN (" . implode( ',', $post_statuses ) . ") AND {$prefix}posts.post_type IN (" . implode( ',', $finalPostTypes ) . ') ) ';
// this OR should be put in place only if there are other enabled post types, else the limiter will get picked up 6 lines down
if ( $finalPostTypesIncludesAttachments ) {
$sql .= ' OR ';
}
}
if ( $finalPostTypesIncludesAttachments ) {
$sql .= "{$prefix}posts.post_type = 'attachment' ";
}
$sql .= ' ) ';
// If something went (very) wrong e.g. there were no post types enabled somehow...
if ( 'AND()' === str_replace( ' ', '', trim( $sql ) ) ) {
$sql = '';
}
return $sql;
}
/**
* Returns the maximum number of pages of results
*
* @return int The total number of pages
* @since 1.0.5
*/
function get_max_num_pages() {
return $this->maxNumPages;
}
/**
* Returns the number of found posts
*
* @return int The total number of posts
* @since 1.0.5
*/
function get_found_posts() {
return $this->foundPosts;
}
/**
* Returns the number of the current page of results
*
* @return int The current page
* @since 1.0.5
*/
function get_page() {
return $this->page;
}
/**
* Returns the SQL being used to get search results
*
* @return string The SQL in use
* @since 2.6
*/
function get_sql() {
return $this->sql;
}
// @codingStandardsIgnoreStart
/**
* @deprecated as of 2.5.7
*/
function queryForPostIDs() {
return $this->query_for_post_ids();
}
/**
* @deprecated as of 2.5.7
*
* @param $settings
*
* @return string
*/
function postStatusLimiterSQL( $settings ) {
return $this->post_status_limiter_sql( $settings );
}
/**
* @deprecated as of 2.5.7
*/
function getMaxNumPages() {
return $this->get_max_num_pages();
}
/**
* @deprecated as of 2.5.7
*/
function getFoundPosts() {
return $this->get_found_posts();
}
/**
* @deprecated as of 2.5.7
*/
function getPage() {
return $this->get_page();
}
// @codingStandardsIgnoreEnd
}