| 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/ |
Upload File : |
<?php
/*
Plugin Name: SearchWP
Plugin URI: https://searchwp.com/
Description: The best WordPress search you can find
Version: 3.1.11
Author: SearchWP, LLC
Author URI: https://searchwp.com/
Text Domain: searchwp
Copyright 2013-2020 SearchWP, LLC
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
// exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'SEARCHWP_VERSION', '3.1.11' );
define( 'SEARCHWP_PREFIX', 'searchwp_' );
define( 'SEARCHWP_DBPREFIX', 'swp_' );
define( 'SEARCHWP_EDD_STORE_URL', 'https://searchwp.com' );
define( 'SEARCHWP_EDD_ITEM_NAME', 'SearchWP' );
// minimum WordPress version requirement
$wp_version = get_bloginfo( 'version' );
if ( version_compare( $wp_version, '3.5', '<' ) ) {
/** @noinspection PhpIncludeInspection */
require_once ABSPATH . '/wp-admin/includes/plugin.php';
deactivate_plugins( __FILE__ );
wp_die( esc_html( __( 'SearchWP requires WordPress 3.5 or higher. Please upgrade before activating this plugin.' ) ) );
}
// includes
include_once( dirname( __FILE__ ) . '/includes/functions.php' );
include_once( dirname( __FILE__ ) . '/includes/class.stats.php' );
if ( ! class_exists( 'SWP_EDD_SL_Plugin_Updater' ) ) {
// load our custom updater
include( dirname( __FILE__ ) . '/lib/SWP_EDD_SL_Plugin_Updater.php' );
}
/**
* Set up the updater
*
* @return SWP_EDD_SL_Plugin_Updater
*/
function searchwp_update_check(){
// retrieve stored license key
$license_key = searchwp_get_license_key();
// instantiate the updater to prep the environment
$searchwp_edd_updater = new SWP_EDD_SL_Plugin_Updater( SEARCHWP_EDD_STORE_URL, __FILE__, array(
'version' => SEARCHWP_VERSION, // current version number
'license' => $license_key, // license key (used get_option above to retrieve from DB)
'item_name' => SEARCHWP_EDD_ITEM_NAME, // name of this plugin
'author' => 'SearchWP, LLC', // author of this plugin
)
);
return $searchwp_edd_updater;
}
add_action( 'admin_init', 'searchwp_update_check' );
global $searchwp;
/**
* Class SearchWP
* @since 1.0
*/
class SearchWP {
/**
* @var string process identifier
* @since 1.5.5
*/
private $pid;
/**
* @var SearchWP The SearchWP singleton
* @since 1.0
*/
private static $instance;
/**
* @var string License key
*/
public $license;
/**
* @var string License status
*/
public $status;
/**
* @var string The plugin directory
* @since 1.0
*/
public $dir;
/**
* @var string The plugin URL
* @since 1.0
*/
public $url;
/**
* @var string The plugin version
* @since 1.0
*/
public $version;
/**
* @var bool Whether a search is taking place right now
* @since 1.0
*/
public $active = false;
/**
* @var bool Whether SearchWP performed a search on this pageload
* @since 1.6.4
*/
public $ran = false;
/**
* @var array Stores diagnostic information for debugging
* @since 1.6.4
*/
public $diagnostics = array();
/**
* @var bool Whether indexing is taking place right now
* @since 1.0.6
*/
public $indexing = false;
/**
* @var bool Whether we're in WordPress' main query
* @since 1.0
*/
public $isMainQuery = false;
/**
* @var string Plugin name
* @since 1.0
*/
public $pluginName = 'SearchWP';
/**
* @var string Plugin textdomain, used in localization
* @since 1.0
*/
public $textDomain = 'searchwp';
/**
* @var array Stores custom field keys
* @since 1.0
*/
public $keys;
/**
* @var array Stores all SearchWP settings
* @since 1.0
*/
public $settings;
/**
* @var array Stores registered post types
*/
public $postTypes = array();
/**
* @var string Stores the original (searched for) query
*/
public $original_query;
/**
* @var array Current, active stopwords
*/
public $common;
/**
* @var array Stopwords
*/
public $stopwords;
/**
* @var array Stores valid weight types
*/
public $validTypes = array( 'content', 'title', 'comment', 'comments', 'tax', 'excerpt', 'cf', 'slug' );
/**
* @var array Stores valid search engine option keys
*/
public $validOptions = array( 'exclude', 'limit_to', 'attribute_to', 'stem', 'parent', 'mimes' );
/**
* @var int Number of posts found in a query
*/
public $foundPosts = 0;
/**
* @var int Number of pages in paginated results
*/
public $maxNumPages = 0;
/**
* @var array Stores a purge queue
* @since 1.0.7
*/
public $purgeQueue = array();
/**
* @var array Database tables utilized
* @since 1.2.3
*/
private $tables = array(
array( 'table' => 'cf', 'exists' => false ), // custom fields
array( 'table' => 'index', 'exists' => false ), // main index
array( 'table' => 'log', 'exists' => false ), // log
array( 'table' => 'tax', 'exists' => false ), // taxonomies
array( 'table' => 'terms', 'exists' => false ), // terms
);
/**
* @var bool Whether the database environment has been properly established
* @since 1.2.3
*/
public $validDatabaseEnvironment = true;
/**
* @var bool Whether the indexer has been paused by the user
* @since 1.4
*/
public $paused = false;
/**
* @var bool Overarching (forceful) condition whether to perform a search on this page load
*/
private $force_run = false;
/**
* @var array Provide a way for regex to be used to extract matches before they're stripped of their punctuation (e.g. dates)
*
* @since 1.9
*/
private $term_pattern_whitelist = array(
// these should go from most strict to most loose
// functions
"/\b(\\w+?)?\\(|[\\s\\n]\\(/is",
// Date formats
'/\b([0-9]{4}-[0-9]{1,2}-[0-9]{1,2})\b/is', // date: YYYY-MM-DD
'/\b([0-9]{1,2}-[0-9]{1,2}-[0-9]{4})\b/is', // date: MM-DD-YYYY
'/\b([0-9]{4}\\/[0-9]{1,2}\\/[0-9]{1,2})\b/is', // date: YYYY/MM/DD
'/\b([0-9]{1,2}\\/[0-9]{1,2}\\/[0-9]{4})\b/is', // date: MM/DD/YYYY
// IP
'/\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\b/is', // IPv4
// initials
"/\\b((?:[A-Za-z]\\.\\s{0,1})+)/isu",
// version numbers: 1.0 or 1.0.4 or 1.0.5b1
'/\b([a-z0-9]+(?:\\.[a-z0-9]+)+)\b/is',
// serial numbers
"/\\b([-_]?[[:alnum:]]+(?:[-_]+[[:alnum:]]+)+[-_]?)\\b/isu", // hyphen/underscore separator
// strings of digits
"/\\b(\\d{1,})\\b/is",
// e.g. M&M, M & M
"/\\b([[:alnum:]]+\\s?(?:&\\s?[[:alnum:]]+)+)\b/isu",
);
/**
* @var bool Whether settings were updated
* @since 1.9.1
*/
public $settings_updated = false;
/**
* @var array Per-post-type-specific weights for search results
* @since 2.3
*/
public $results_weights = array();
/**
* @var string User capability to modify SearchWP settings in the WordPress admin
* @since 2.1
*/
public $settings_cap = 'manage_options';
/**
* @var string Endpoint used when indexing (prefixed by site_url())
* @since 2.3
*/
public $endpoint = '';
/**
* @var bool Whether the alternate indexer should be used
* @since 2.5
*/
private $alternate_indexer = false;
/**
* @var array Any search query modifications that were applied (e.g. min word length)
* @since 2.5
*/
private $search_query_mods = array();
/**
* @var SearchWP_Admin_Settings Settings UI utility class
* @since 2.6
*/
private $settings_utils;
/**
* @var SearchWP_i18n utility class
* @since 2.9
*/
public $i18n;
/**
* var SearchWP_Admin_Ajax utility class
* @since 2.9
*/
public $ajax;
/**
* @var SearchWP_Nags utility class to help with notifications in the admin
* @since 2.6
*/
private $nags_utils;
/**
* @var string The SQL used to generate this set of results
* @since 2.5
*/
private $search_sql;
/**
* @var array Synonyms
* @since 3.0
*/
public $synonyms;
/**
* @var array Suggested Terms
* @since 3.1
*/
private $suggested_terms;
private $debug;
private $validated_terms = array();
public $partial_matches;
/**
* Singleton
*
* @return SearchWP
* @since 1.0
*/
public static function instance() {
if ( ! isset( self::$instance ) && ! ( self::$instance instanceof SearchWP ) ) {
// store background indexer request
if ( isset( $_REQUEST['swpnonce'] ) ) {
searchwp_delete_option( 'indexnonce' );
searchwp_add_option( 'indexnonce', sanitize_text_field( $_REQUEST['swpnonce'] ) );
}
self::$instance = new SearchWP;
self::$instance->init();
// process the purge queue once everything is said and done
add_action( 'shutdown', array( self::$instance, 'setup_purge_queue' ) );
add_action( 'after_setup_theme', array( self::$instance, 'set_settings_cap' ) );
}
return self::$instance;
}
/**
* Set the capability necessary for interacting with SearchWP's settings in the WordPress admin
*
* @since 2.1
*/
function set_settings_cap() {
$this->settings_cap = apply_filters( 'searchwp_settings_cap', $this->settings_cap );
}
public function get_suggested_terms() {
return $this->suggested_terms;
}
/**
* Initialization routine. Sets version, directory, url, adds WordPress hooks, includes includes, triggers index
*
* @uses get_post_types to determine which post types are in use
* @since 1.0
*/
function init() {
$this->version = SEARCHWP_VERSION;
$this->dir = dirname( __FILE__ );
$this->url = plugin_dir_url( __FILE__ );
$this->pid = str_replace( '.', '', uniqid( 'swppid', true ) );
// includes
include_once( dirname( __FILE__ ) . '/includes/class.i18n.php' );
include_once( dirname( __FILE__ ) . '/includes/class.debug.php' );
include_once( dirname( __FILE__ ) . '/includes/class.stopwords.php' );
include_once( dirname( __FILE__ ) . '/includes/class.stemmer.php' );
include_once( dirname( __FILE__ ) . '/includes/class.document-parser.php' );
include_once( dirname( __FILE__ ) . '/includes/class.indexer.php' );
include_once( dirname( __FILE__ ) . '/templates/tmpl.engine.config.php' );
include_once( dirname( __FILE__ ) . '/templates/tmpl.supplemental.config.php' );
include_once( dirname( __FILE__ ) . '/includes/class.search.php' );
include_once( dirname( __FILE__ ) . '/includes/class.upgrade.php' );
include_once( dirname( __FILE__ ) . '/includes/class.synonyms.php' );
include_once( dirname( __FILE__ ) . '/includes/class.highlighter.php' );
include_once( dirname( __FILE__ ) . '/includes/class.partial-matches.php' );
include_once( dirname( __FILE__ ) . '/admin/class.admin-settings.php' );
include_once( dirname( __FILE__ ) . '/admin/class.extensions.php' );
include_once( dirname( __FILE__ ) . '/admin/class.nags.php' );
// Integrations.
include_once( dirname( __FILE__ ) . '/includes/class.acf.php' );
// Implement debugging early if applicable.
$early_debug_enabled = apply_filters( 'searchwp_debug_early', false );
if ( $early_debug_enabled ) {
$advanced_settings_ref = get_option( SEARCHWP_PREFIX . 'advanced' );
if ( ! empty( $advanced_settings_ref['debugging'] ) ) {
$this->init_debugging();
}
}
if ( is_admin() ) {
include_once( dirname( __FILE__ ) . '/admin/class.conflicts.php' );
include_once( dirname( __FILE__ ) . '/admin/class.notices.php' );
include_once( dirname( __FILE__ ) . '/admin/class.dashboard.php' );
include_once( dirname( __FILE__ ) . '/admin/class.systeminfo.php' );
include_once( dirname( __FILE__ ) . '/admin/class.ajax.php' );
// Ajax utility library
$this->ajax = new SearchWP_Admin_Ajax();
$this->ajax->init();
}
include_once( dirname( __FILE__ ) . '/includes/class.swp-query.php' );
// instantiate nags
$this->nags_utils = new SearchWP_Nags();
$this->nags_utils->init();
// grab our settings
$this->settings = get_option( SEARCHWP_PREFIX . 'settings' );
$this->license = searchwp_get_license_key();
$this->status = get_option( SEARCHWP_PREFIX . 'license_status' );
// append our indexer-specific settings since they're stored separately
if ( $indexer_settings = get_option( SEARCHWP_PREFIX . 'indexer' ) ) {
if ( ! is_array( $indexer_settings ) ) {
$this->trigger_forced_indexer_chunk();
$indexer_settings = get_option( SEARCHWP_PREFIX . 'indexer' );
}
$this->settings = array_merge( $this->settings, $indexer_settings );
}
// instantiate settings UI
$this->settings_utils = new SearchWP_Admin_Settings();
$this->settings_utils->init();
// i18n
$this->i18n = new SearchWP_i18n();
$this->i18n->init();
// Synonyms
$this->synonyms = new SearchWP_Synonyms();
$this->synonyms->init();
// Partial matches.
$this->partial_matches = new SearchWPPartialMatches();
// Implement advanced settings.
$legacy_advanced_settings = apply_filters( 'searchwp_legacy_advanced_settings', false );
if ( empty( $legacy_advanced_settings ) ) {
include_once dirname( __FILE__ ) . '/admin/settings-impl-advanced.php';
} else {
include_once dirname( __FILE__ ) . '/admin/settings-impl-advanced-legacy.php';
}
// Implement license management
include_once dirname( __FILE__ ) . '/admin/settings-impl-license.php';
include_once dirname( __FILE__ ) . '/includes/class.license.php';
// introduced in version 2.5.7 as per WordPress 4.2
$this->settings['utf8mb4'] = get_option( SEARCHWP_PREFIX . 'utf8mb4' );
// hooks
add_filter( 'block_local_requests', '__return_false' );
add_action( 'admin_menu', array( $this, 'admin_menu' ) );
add_action( 'admin_init', array( $this, 'activation' ) );
add_action( 'admin_init', array( $this, 'init_settings' ) );
add_filter( 'cron_schedules', array( $this, 'add_custom_cron_interval' ) );
add_action( 'admin_init', array( $this, 'schedule_maintenance' ) );
add_action( 'swp_indexer', array( $this, 'do_cron' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'assets' ) );
add_filter( 'heartbeat_received', array( $this, 'heartbeat_received' ), 10, 2 );
add_action( 'pre_get_posts', array( $this, 'check_for_main_query' ), 0 );
add_action( 'pre_get_posts', array( $this, 'impose_engine_config' ), 20 );
add_filter( 'request', array( $this, 'filter_request' ) );
add_filter( 'the_posts', array( $this, 'wp_search' ), 0, 2 );
add_filter( 'posts_request', array( $this, 'maybe_cancel_wp_query' ), 10, 2 );
add_action( 'add_meta_boxes', array( $this, 'document_content_meta_box' ) );
add_action( 'edit_attachment', array( $this, 'document_content_save' ) );
add_action( 'wp_before_admin_bar_render', array( $this, 'admin_bar_menu' ) );
add_action( 'shutdown', array( $this, 'shutdown' ), 9999 );
add_action( 'wp_footer', array( $this, 'maybe_output_debug' ) );
add_action( 'wp_footer', array( $this, 'admin_bar_entry_for_search' ) );
add_action( 'wp_loaded', array( $this, 'load' ) );
add_action( 'init', array( $this, 'prepare_endpoint' ) );
add_filter( 'plugin_action_links_searchwp/searchwp.php', array( $this, 'plugin_update_link' ) );
// license maintenance (triggered by cron job)
$license_utils = new SearchWP_Settings_Implementation_License();
add_action( 'swp_maintenance', array( $license_utils, 'do_maintenance' ) );
// support WordPress Importer by auto-pausing during imports
add_action( 'import_start', array( $this, 'indexer_pause' ) );
add_action( 'import_end', array( $this, 'indexer_unpause' ) );
add_action( 'wp_ajax_searchwp_alternate_indexer_trigger', array( $this, 'handle_alternate_indexer_request' ) );
// internal hooks
add_filter( 'searchwp_results', array( $this, 'maybe_append_weight_to_result_title' ), 10, 2 );
// core filter for Media search in the admin
add_filter( 'ajax_query_attachments_args', array( $this, 'maybe_admin_media_search' ) );
}
function init_debugging() {
// If debugging has already been initialized, bail out.
if ( ! empty( $this->debug ) ) {
return;
}
$wp_upload_dir = wp_upload_dir();
$this->debug = new SearchWPDebug();
$this->debug->init( $wp_upload_dir['basedir'] );
}
/**
* WordPress adds some query vars for hierarchical post types when searching in the admin
* but that interferes with SearchWP's process, so we need to remove the flag that causes trouble
*
* @since 2.9.4
*/
function filter_request( $query_vars ) {
$search_in_admin = apply_filters( 'searchwp_in_admin', false );
if ( ! is_admin() || empty( $search_in_admin ) ) {
return $query_vars;
}
if ( ! isset( $query_vars['fields'] ) ) {
return $query_vars;
}
// If 'fields' is set, WP_Query ends up essentially short circuiting when we don't want it to
unset( $query_vars['fields'] );
return $query_vars;
}
/**
* SearchWP allows for developers to set a custom endpoint for indexer communication
*
* @since 2.3
*/
function prepare_endpoint() {
// allow developer to customize the endpoint
$this->endpoint = urlencode( apply_filters( 'searchwp_endpoint', '' ) );
// if there's an endpoint (as opposed to site_url() which is the default) we need to add it
if ( strlen( $this->endpoint ) ) {
// note: we're already in the 'init' action so we're going to add our rewrite rule right here
add_rewrite_rule( '^' . $this->endpoint . '/?', 'index.php?__searchwp_api', 'top' );
// after this rewrite rule gets hit, the indexer die()s so we don't need a handler
}
// if the filtered endpoint does not match the saved endpoint in the settings, we need to flush our rules
if ( ! isset( $this->settings['endpoint'] ) ) {
$this->settings['endpoint'] = '';
}
if ( $this->endpoint !== $this->settings['endpoint'] ) {
$this->settings['endpoint'] = sanitize_text_field( $this->endpoint ); // overwrite the setting
searchwp_set_setting( 'endpoint', $this->endpoint ); // persist the setting
flush_rewrite_rules(); // flush the rules
}
// finalize the endpoint by prefixing the site_url()
if ( strlen( $this->endpoint ) ) {
$this->endpoint = trailingslashit( $this->endpoint );
} else {
$this->endpoint = 'index.php';
}
// accommodate HTTP Basic Auth
$root_url = trailingslashit( site_url() );
$http_basic_auth_creds = apply_filters( 'searchwp_basic_auth_creds', false );
if ( is_array( $http_basic_auth_creds ) && isset( $http_basic_auth_creds['username'] ) && isset( $http_basic_auth_creds['password'] ) ) {
$credentials_prepared = urlencode( $http_basic_auth_creds['username'] ) . ':' . urlencode( $http_basic_auth_creds['password'] );
$root_url = str_replace( '//', '//' . $credentials_prepared . '@', $root_url );
add_filter( 'searchwp_indexer_loopback_args', array( $this, 'insert_http_auth_creds' ) );
}
$this->endpoint = esc_url_raw( $root_url . $this->endpoint );
}
/**
* Callback that inserts HTTP Basic Auth credentials into indexer request headers
*
* @since 2.3.4
*
* @param $args
*
* @return mixed
*/
function insert_http_auth_creds( $args ) {
$http_basic_auth_creds = apply_filters( 'searchwp_basic_auth_creds', false );
if ( is_array( $http_basic_auth_creds ) && isset( $http_basic_auth_creds['username'] ) && isset( $http_basic_auth_creds['password'] ) ) {
$args['headers'] = array(
'Authorization' => 'Basic ' . base64_encode( $http_basic_auth_creds['username'] . ':' . $http_basic_auth_creds['password'] )
);
}
return $args;
}
/**
* Perform various environment checks/initializations on wp_loaded
*
* @since 1.8
*/
function load() {
global $wp_query;
// Proceed with loading SearchWP.
if ( apply_filters( 'searchwp_debug', false ) ) {
$this->init_debugging();
}
$this->prepare_endpoint();
// set the registered post types
$this->postTypes = array_merge(
array(
'post' => 'post',
'page' => 'page',
'attachment' => 'attachment',
),
get_post_types(
array(
'exclude_from_search' => false,
'_builtin' => false,
)
)
);
// check for upgrade
new SearchWPUpgrade( $this->version );
// ensure working database environment
$this->check_database_environment();
// devs can customize which post types are indexed, it doesn't make
// sense to list post types that were excluded (or included (e.g. post types that don't
// allow filtration of the exclude_from_search arg))
$this->postTypes = $this->get_indexed_post_types();
// if the settings were somehow edited directly in the database
$this->settings = SWP()->validate_settings( $this->settings );
// allow filtration of what SearchWP considers common words (i.e. ignores)
$this->stopwords = new SearchWP_Stopwords();
$this->common = $this->stopwords->localized();
$this->alternate_indexer = apply_filters( 'searchwp_alternate_indexer', false );
// one-stop filter to ensure SearchWP fires
if ( $this->force_run = apply_filters( 'searchwp_force_run', $this->force_run ) ) {
$wp_query->is_search = true;
}
$this->check_if_paused();
$this->set_index_update_triggers();
do_action( 'searchwp_load' );
// handle index and/or purge requests
$this->update_index();
// reset short circuit check
$this->indexing = false;
// Set up global highlighter functions.
if ( function_exists( 'searchwp_init_global_highlight_functions' ) ) {
searchwp_init_global_highlight_functions();
}
do_action( 'searchwp_loaded' );
}
function validate_purge_queue( $purge_queue ) {
if ( ! is_array( $purge_queue ) ) {
$purge_queue = array();
}
if ( empty( $purge_queue ) ) {
return (array) $purge_queue;
}
// If there are duplicate purge queue entries we could very well trigger an infinite loop of delta updates
$purge_queue = array_unique( $purge_queue );
// Make sure items in the purge queue are not set to be excluded
$excluded_ids = apply_filters( 'searchwp_exclude', array(), '', array() );
$excluded_ids = array_map( 'absint', $excluded_ids );
if ( ! empty( $excluded_ids ) ) {
$purge_queue = array_diff( $purge_queue, $excluded_ids );
}
$aggressive_cleanup = apply_filters( 'searchwp_aggressive_delta_update', false );
if ( ! empty( $aggressive_cleanup ) ) {
// Anything that's excluded can be removed from the index
$excluded_ids_in_index = $this->filter_posts_in_index( $excluded_ids );
// Purge anything that is to be excluded anyway
if ( ! empty( $excluded_ids_in_index ) ) {
foreach ( $excluded_ids_in_index as $excluded_id ) {
do_action( 'searchwp_log', 'Purge post (via validate_purge_queue) ' . $excluded_id );
$this->purge_post( $excluded_id, true );
}
}
}
return $purge_queue;
}
function filter_posts_in_index( $post_ids ) {
global $wpdb;
if ( ! is_array( $post_ids ) ) {
$post_ids = array( $post_ids );
}
$post_ids = array_map( 'absint', $post_ids );
$post_ids = array_unique( $post_ids );
$post_ids_sql = implode( ', ', $post_ids );
$index_table = $wpdb->prefix . SEARCHWP_DBPREFIX . 'index';
$ids_in_index_sql = "SELECT post_id FROM {$index_table} WHERE post_id IN ({$post_ids_sql}) GROUP BY post_id LIMIT 1000";
$already_indexed = $wpdb->get_col( $ids_in_index_sql );
$already_indexed = array_map( 'absint', $already_indexed );
return $already_indexed;
}
/**
* SearchWP queues up post objects that must be purged, this function records them
*
* @since 1.3.1
*/
function setup_purge_queue() {
if ( ! empty( $this->purgeQueue ) ) {
do_action( 'searchwp_log', 'setup_purge_queue() ' . count( $this->purgeQueue ) );
$existingPurgeQueue = searchwp_get_option( 'purge_queue' );
if ( is_array( $existingPurgeQueue ) && ! empty( $existingPurgeQueue ) ) {
foreach ( $existingPurgeQueue as $postToPurge ) {
$postToPurge = absint( $postToPurge );
if ( ! isset( $this->purgeQueue[ $postToPurge ] ) ) {
$this->purgeQueue[ $postToPurge ] = $postToPurge;
}
}
}
if ( is_array( $this->purgeQueue ) && ! empty( $this->purgeQueue ) ) {
foreach ( $this->purgeQueue as $key => $postToPurge ) {
if ( ! $this->is_applicable_post_status( $postToPurge ) ) {
do_action( 'searchwp_log', 'Prevented delta update (blacklisted post status) ' . $postToPurge );
unset( $this->purgeQueue[ $key ] );
}
}
}
// if the alternative indexer is in play, we need to process
// the purge queue right now instead of relying on background processing
if ( $this->alternate_indexer ) {
do_action( 'searchwp_log', 'BEGIN processing purge queue' );
foreach ( $this->purgeQueue as $post_to_purge ) {
$this->purge_post( absint( $post_to_purge ) );
do_action( 'searchwp_log', 'Purged ' . absint( $post_to_purge ) );
}
do_action( 'searchwp_log', 'END processing purge queue' );
$this->trigger_index();
// now that the purge queue has been processed, we need to clear it
// out so as to prevent the purge collection from taking place
$this->purgeQueue = array();
}
searchwp_update_option( 'purge_queue', $this->purgeQueue );
}
}
/**
* Callback to WordPress' shutdown action, used to ensure only a single SearchWP process was running
*
* @since 1.5.5
*/
function shutdown( $forced = false ) {
if ( $forced ) {
return;
}
// do_action( 'searchwp_log', ' ' );
// do_action( 'searchwp_log', '========== END ' . $this->pid . ' ==========' );
// do_action( 'searchwp_log', ' ' );
}
/**
* Getter for the pid
*
* @return string process ID
* @since 1.5.5
*/
function get_pid() {
return $this->pid;
}
/**
* Implement necessary hooks for delta index updates
*
* @since 1.8
*/
function set_index_update_triggers() {
$prevent_triggers = searchwp_get_option( 'prevent_delta_triggers' );
if ( ! empty( $prevent_triggers ) ) {
do_action( 'searchwp_log', 'Skipping index update triggers' );
return;
}
add_action( 'save_post', array( $this, 'purge_post_via_edit' ), 999 );
// Trigger the index when a Scheduled post is Published.
add_action( 'publish_future_post', array( $this, 'trigger_index' ), 999 );
if ( current_user_can( 'edit_posts' ) ) {
add_action( 'add_attachment', array( $this, 'purge_post_via_edit' ), 999 );
add_action( 'edit_attachment', array( $this, 'purge_post_via_edit' ), 999 );
}
if ( current_user_can( 'delete_posts' ) ) {
add_action( 'before_delete_post', array( $this, 'purge_post_via_edit' ), 999 );
}
// Update the index when a new comment is posted
add_action( 'comment_post', array( $this, 'purge_post_via_comment_post' ), 10, 2 );
// Update the index when a comment is approved (or unapproved)
add_action( 'transition_comment_status', array( $this, 'purge_post_via_comment_transition' ), 10, 3 );
// we want to purge a post from the index when comments are manipulated
add_action( 'edit_comment', array( $this, 'purge_post_via_comment' ) );
add_action( 'trash_comment', array( $this, 'purge_post_via_comment' ) );
add_action( 'delete_comment', array( $this, 'purge_post_via_comment' ) );
add_action( 'delete_attachment', array( $this, 'purge_post_via_edit' ), 999 );
// purge a post from the index when a related term is deleted
add_action( 'set_object_terms', array( $this, 'purge_post_via_term' ), 10, 6 );
}
/**
* Add an Update force check on the plugin page
*
* @param $links
*
* @return array Links to include on the Plugins page
*/
function plugin_update_link( $links ) {
if ( current_user_can( apply_filters( 'searchwp_settings_cap', 'manage_options' ) ) ) {
$settings_link = admin_url( 'options-general.php?page=searchwp' );
$settings_link = '<a href="' . esc_url( $settings_link ) . '">' . __( 'Settings', 'searchwp' ) . '</a>';
array_unshift( $links, $settings_link );
}
return $links;
}
/**
* Outputs HTML comments containing diagnostic information about what took place during a single pageload
*
* @since 1.6.4
*/
function maybe_output_debug() {
if ( apply_filters( 'searchwp_debug', false ) ) { ?>
<!-- [SearchWP] Debug Information
SearchWP performed a search: <?php echo $this->ran ? 'Yes' : 'No'; ?>
Searches performed: <?php echo count( $this->diagnostics ); ?>
<?php $searchCount = 1; foreach ( $this->diagnostics as $diagnostics ) : ?>
== SEARCH <?php echo esc_html( $searchCount ); ?> ==
Search Engine: <?php echo ( isset( $diagnostics['engine'] ) ) ? esc_html( $diagnostics['engine'] ) : '[[ERROR]]'; ?>
Accepted search terms: <?php echo ( is_array( $diagnostics['terms'] ) && ! empty( $diagnostics['terms'] ) ) ? esc_html( implode( ' ', $diagnostics['terms'] ) ) : '[[NONE]]'; ?>
Total results found: <?php echo ( isset( $diagnostics['found_posts'] ) ) ? esc_html( $diagnostics['found_posts'] ) : '[[ERROR]]'; ?>
Total query time: <?php echo ( isset( $diagnostics['profiler'] ) ) ? esc_html( floatval( $diagnostics['profiler']['after'] ) - floatval( $diagnostics['profiler']['before'] ) ) : '[[ERROR]]'; ?>s
Results in this set:
<?php
// grab just post IDs and titles
$postsArePosts = true;
include_once( dirname( __FILE__ ) . '/lib/class.consoletable.php' );
$debug_table = new SearchWPConsoleTable();
if ( is_array( $diagnostics['posts'] ) && isset( $diagnostics['posts'][0] ) ) {
if ( is_numeric( $diagnostics['posts'][0] ) ) {
// developer wanted only post IDs
$postsArePosts = false;
}
$debug_table->add_header( __( 'Post ID', 'searchwp' ) );
$debug_table->add_header( __( 'Weight', 'searchwp' ) );
$debug_table->add_header( __( 'Title', 'searchwp' ) );
foreach ( $diagnostics['posts'] as $key => $post ) {
// get the proper ID and title
if ( $postsArePosts ) {
$post_id = $post->ID;
$post_title = $post->post_title;
} else {
$post_id = $post;
$post_title = get_the_title( $post );
}
// the search just ran so we can reference the result weights
$weights = $this->results_weights;
$post_weight = array_key_exists( $post_id, $weights ) ? absint( $weights[ $post_id ]['weight'] ) : 0;
$debug_table->add_row();
$debug_table->add_column( absint( $post_id ) );
$debug_table->add_column( absint( $post_weight ) );
$debug_table->add_column( esc_html( strip_tags( $post_title ) ) );
}
echo "\n";
echo $debug_table->get_table();
echo "\n\n";
} else {
echo '[[NONE]]';
}
?>
<?php $searchCount++; endforeach; ?>
-->
<?php }
}
/**
* Add the SearchWP Admin Bar root menu
*
* @since 1.5
*
* @param $name
* @param $id
* @param bool $href
*/
function admin_bar_add_root_menu( $name, $id, $href = false ) {
global $wp_admin_bar;
if ( ! is_admin_bar_showing() ) {
return;
}
if ( method_exists( $wp_admin_bar, 'add_menu' ) ) {
$wp_admin_bar->add_menu( array(
'id' => $id,
'meta' => array(),
'title' => $name,
'href' => $href,
)
);
}
}
/**
* Add a SearchWP Admin Bar sub menu
*
* @since 1.5
*
* @param $name
* @param $link
* @param $root_menu
* @param $id
* @param bool $meta
*/
function admin_bar_add_sub_menu( $name, $link, $root_menu, $id, $meta = false ) {
global $wp_admin_bar;
if ( ! is_admin_bar_showing() ) {
return;
}
if ( method_exists( $wp_admin_bar, 'add_menu' ) ) {
$wp_admin_bar->add_menu( array(
'parent' => $root_menu,
'id' => $id,
'title' => $name,
'href' => $link,
'meta' => $meta,
)
);
}
}
/**
* Determine the last time a post was indexed
*
* @since 1.5
*
* @param $post_id
* @param bool $timeDiff
*
* @return bool|int|string
*/
function get_last_indexed_time( $post_id, $timeDiff = false ) {
if ( empty( $post_id ) ) {
do_action( 'searchwp_log', 'No $post_id provided' );
return false;
}
$lastIndex = get_post_meta( $post_id, '_' . SEARCHWP_PREFIX . 'last_index', true );
$timestamp = ( ! empty( $lastIndex ) ) ? absint( $lastIndex ) : false;
$timestamp = ( $timeDiff && $timestamp ) ? human_time_diff( date( 'U', $timestamp ), current_time( 'timestamp' ) ) . __( ' ago', 'searchwp' ) : $timestamp;
if ( ! empty( $timestamp ) ) {
do_action( 'searchwp_log', 'get_last_indexed_time(): ' . print_r( $timestamp, true ) );
}
return $timestamp;
}
/**
* Callback to implement the SearchWP Admin Bar menu item
*
* @since 1.5
*/
function admin_bar_menu() {
global $pagenow, $post, $wpdb;
if ( ! apply_filters( 'searchwp_admin_bar', true ) ) {
return;
}
// only show in the admin and if user can manage options
if ( ! is_admin() || ! current_user_can( $this->settings_cap ) ) {
return;
}
// only show if user can manage
// root menu
$this->admin_bar_add_root_menu(
'SearchWP',
$this->textDomain,
esc_url( get_admin_url() . 'options-general.php?page=' . $this->textDomain )
);
// settings
$settings_label = __( 'Settings', 'searchwp' );
$this->admin_bar_add_sub_menu(
$settings_label,
esc_url( get_admin_url() . 'options-general.php?page=' . $this->textDomain ),
$this->textDomain,
$this->textDomain . '_settings'
);
// pause toggle
$saved_settings = searchwp_get_option( 'advanced' );
$toggleLabel = isset( $saved_settings['disable_indexer'] ) && ! empty( $saved_settings['disable_indexer'] ) ? __( 'Enable Indexer', 'searchwp' ) : __( 'Disable Indexer', 'searchwp' );
$this->admin_bar_add_sub_menu(
$toggleLabel,
esc_url( add_query_arg( 'swppausenonce', wp_create_nonce( 'swppausenonce' ) ) ),
$this->textDomain,
$this->textDomain . '_toggle_pause'
);
// last indexed
switch ( $pagenow ) {
case 'post.php':
do_action( 'searchwp_log', 'Current page is post.php' );
if ( isset( $post->ID ) ) {
do_action( 'searchwp_log', '$post->ID = ' . $post->ID );
// we need to pull the purge queue manually to see if this post is currently waiting to be indexed
$tmpPurgeQueue = searchwp_get_option( 'purge_queue' );
if ( ! empty( $tmpPurgeQueue ) ) {
if ( is_array( $tmpPurgeQueue ) ) {
do_action( 'searchwp_log', 'Temporary purge queue: ' . implode( ', ', $tmpPurgeQueue ) );
} else {
do_action( 'searchwp_log', 'Temporary purge queue: ' . print_r( $tmpPurgeQueue, true ) );
}
}
// if we happen to be viewing an edit screen for a post in line to be indexed, say so
if ( is_array( $tmpPurgeQueue ) && in_array( $post->ID, $tmpPurgeQueue ) ) {
do_action( 'searchwp_log', 'Currently being indexed' );
$lastIndexedMessage = __( 'In index queue', 'searchwp' );
} else {
// last indexed
$lastIndexed = $this->get_last_indexed_time( $post->ID, true );
// there's a chance this functionality was added after a post actually was indexed, so let's check for that
if ( ! $lastIndexed ) {
// see if this post ID is in the index
$post->ID = absint( $post->ID );
$postInIndex = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}swp_index WHERE {$wpdb->prefix}swp_index.post_id = %d LIMIT 1", absint( $post->ID ) ) );
if ( ! empty( $postInIndex ) ) {
// if there's a term cache the post is getting chunked right now
$stored_terms = get_post_meta( $post->ID, '_' . SEARCHWP_PREFIX . 'terms', true );
if ( ! empty( $stored_terms ) ) {
$lastIndexedMessage = __( 'This entry is being indexed', 'searchwp' );
} else {
$lastIndexedMessage = __( 'This entry is indexed', 'searchwp' );
}
} else {
// check to see if we can give some context as to why a post is not indexed
$status = get_post_status( $post->ID );
$applicable_post_statuses = (array) apply_filters( 'searchwp_post_statuses', array( 'publish' ), null );
if ( ! in_array( $status, $applicable_post_statuses ) ) {
$lastIndexedMessage = __( 'Not indexed ', 'searchwp' ) . '(' . __( 'status is', 'searchwp' ) . ': <code>' . esc_html( $status ) . '</code>)';
} else {
// some unknown problem
$lastIndexedMessage = __( 'Not indexed', 'searchwp' );
}
}
} else {
$lastIndexedMessage = __( 'Last indexed', 'searchwp' ) . ' ' . $lastIndexed;
}
do_action( 'searchwp_log', strip_tags( $lastIndexedMessage ) );
}
// add the menu item
$this->admin_bar_add_sub_menu(
$lastIndexedMessage,
null,
$this->textDomain,
$this->textDomain . '_last_indexed'
);
} else {
do_action( 'searchwp_log', '$post->ID was not defined' );
}
break;
}
// link to stats
$this->admin_bar_add_sub_menu(
__( 'Statistics', 'searchwp' ),
esc_url( get_admin_url() . 'index.php?page=searchwp-stats' ),
$this->textDomain,
$this->textDomain . '_stats'
);
}
/**
* Pause the indexer programmatically
*
* @since 1.5
*
* Updated in 2.8.2 to work with the Advanced tab toggles instead of it's own option
*/
function indexer_pause() {
do_action( 'searchwp_log', 'indexer_pause()' );
$this->paused = true;
$saved_settings = searchwp_get_option( 'advanced' );
if ( ! is_array( $saved_settings ) ) {
$saved_settings = array();
}
$saved_settings['disable_indexer'] = $this->paused;
searchwp_update_option( 'advanced', $saved_settings );
}
/**
* Unpause the indexer programmatically
*
* @since 1.5
*/
function indexer_unpause() {
do_action( 'searchwp_log', 'indexer_unpause()' );
$this->paused = false;
$saved_settings = searchwp_get_option( 'advanced' );
if ( ! is_array( $saved_settings ) ) {
$saved_settings = array();
}
$saved_settings['disable_indexer'] = $this->paused;
searchwp_update_option( 'advanced', $saved_settings );
$this->trigger_index();
}
/**
* Called from the Advanced Settings page, toggles the global indexer pause flag
*
* @since 1.4
*/
function check_if_paused() {
$saved_settings = searchwp_get_option( 'advanced' );
$paused = isset( $saved_settings['disable_indexer'] ) && ! empty( $saved_settings['disable_indexer'] );
$this->paused = empty( $paused ) ? false : true;
if (
( ( isset( $_REQUEST['nonce'] ) && wp_verify_nonce( $_REQUEST['nonce'], 'swpadvanced' ) ) &&
( isset( $_REQUEST['action'] ) && wp_verify_nonce( $_REQUEST['action'], 'swppauseindexer' ) )
&& current_user_can( $this->settings_cap ) )
||
( ( isset( $_REQUEST['swppausenonce'] ) && wp_verify_nonce( $_REQUEST['swppausenonce'], 'swppausenonce' ) )
&& current_user_can( $this->settings_cap ) )
) {
if ( $this->paused ) {
$this->indexer_unpause();
} else {
$this->indexer_pause();
}
}
// allow devs to pause the indexer in realtime
$this->paused = apply_filters( 'searchwp_indexer_paused', $this->paused ); // legacy
// enabled is the opposite of paused (if indexer is paused, it should not be enabled)
$maybe_paused = is_bool( $this->paused ) ? $this->paused : false;
if ( ! $maybe_paused ) {
$this->paused = ! apply_filters( 'searchwp_indexer_enabled', true );
}
if ( $this->paused ) {
do_action( 'searchwp_log', 'Checking indexer: PAUSED' );
}
}
/**
* Fire request to validate database environment and take proper action if requirements aren't met
*
* @since 1.3.1
*/
function check_database_environment() {
global $wpdb;
// make sure the database environment is proper
if ( false == searchwp_get_setting( 'valid_db_environment' ) ) {
do_action( 'searchwp_log', 'check_database_environment(): Database environment unconfirmed' );
$this->validate_database_environment();
}
if ( is_admin() && current_user_can( 'manage_options' ) && ! $this->validDatabaseEnvironment ) {
do_action( 'searchwp_log', 'check_database_environment(): Database environment invalid' );
// automatically deactivate
/** @noinspection PhpIncludeInspection */
require_once ABSPATH . '/wp-admin/includes/plugin.php';
deactivate_plugins( __FILE__ );
// determine which table(s) were not created
$tables = array();
foreach ( $this->tables as $table ) {
if ( false === $table['exists'] ) {
$tables[] = esc_html( $wpdb->prefix . SEARCHWP_DBPREFIX . $table['table'] );
}
}
$message = __( '<p>SearchWP <strong>has been automatically deactivated</strong> because it failed to create necessary database table(s):</p>', 'searchwp' );
$message .= '<ul><li><code>' . implode( '</code></li><li><code>', $tables ) . '</code></li></ul>';
$message .= __( '<p>Please ensure the applicable MySQL user has <code>CREATE</code> permissions and try activating again.</p>', 'searchwp' );
$message .= '<p><a href="' . esc_url( trailingslashit( get_admin_url( 'plugins.php' ) ) ) . '">' . __( 'Back to Plugins', 'searchwp' ) . '</a></p>';
// In order for the installation routine to run again we need to remove the version flag
delete_option( SEARCHWP_PREFIX . 'version' );
// output helpful message and die
do_action( 'searchwp_log', 'Shutting down after discovering invalid database environment' );
$this->shutdown( true );
wp_die( $message );
}
}
/**
* Potentially process background index/purge requests
*
* @since 1.3.1
*/
function update_index() {
searchwp_check_for_stalled_indexer( 360 );
// store the purge queue... just in case
$toPurge = (array) searchwp_get_option( 'purge_queue' );
$validated_purge_queue = (array) $this->validate_purge_queue( $toPurge );
if ( is_array( $toPurge ) && count( $toPurge ) !== count( $validated_purge_queue ) ) {
searchwp_update_option( 'purge_queue', $validated_purge_queue );
$toPurge = $validated_purge_queue;
do_action( 'searchwp_log', 'Refined purge queue (' . count( $toPurge ) . '): ' . implode( ', ', $toPurge ) );
}
$busy = searchwp_get_option( 'busy' );
if ( false !== wp_cache_get( 'searchwp_transient', 'options' ) ) {
wp_cache_delete( 'searchwp_transient', 'options' );
}
$purge_transient = get_option( 'swppurge_transient' );
$purge_nonce = isset( $_REQUEST['swppurge'] ) ? sanitize_text_field( $_REQUEST['swppurge'] ) : '';
if ( isset( $_REQUEST['swppurge'] ) && $purge_transient !== $purge_nonce ) {
do_action( 'searchwp_log', 'Purge nonce MISMATCH' );
die();
}
// trigger background indexing
if ( isset( $_REQUEST['swppurge'] ) && $purge_transient === $purge_nonce ) {
if ( is_array( $toPurge ) && ! empty( $toPurge ) ) {
do_action( 'searchwp_log', 'Purge queue (' . count( $toPurge ) . '): ' . implode( ', ', $toPurge ) );
foreach ( $toPurge as $object_id ) {
do_action( 'searchwp_log', 'Purge post ' . $object_id );
$this->purge_post( intval( $object_id ) );
}
searchwp_update_option( 'doing_delta', false );
} else {
do_action( 'searchwp_log', 'Delta queue is inapplicable' );
}
searchwp_update_option( 'purge_queue', array() );
$this->purgeQueue = array();
do_action( 'searchwp_log', 'Purge queue processed, trigger_index()' );
// allow developers the ability to disable automatic reindexing after edits in favor of their own method
$automaticallyReindex = apply_filters( 'searchwp_auto_reindex', true );
do_action( 'searchwp_log', '$automaticallyReindex = ' . print_r( $automaticallyReindex, true ) );
if ( ! $busy && ! $this->paused && $automaticallyReindex ) {
// if the initial index hasn't been built yet, we don't want this request to double up
// in the use case where the user is editing posts while the initial index is still being built
if ( searchwp_get_setting( 'initial_index_built' ) ) {
// @since 2.9.7 the index trigger was moved to later in the request
do_action( 'searchwp_log', 'Shutting down after purge request and index trigger' );
$this->trigger_index();
}
}
// The indexer is running in the background, but the purge queue state is still not closed
die();
} elseif ( ! $this->paused && ! $this->indexing && ! empty( $_REQUEST['swpnonce'] ) && get_option( 'searchwp_transient' ) === sanitize_text_field( $indexnonce = searchwp_get_option( 'indexnonce' ) ) ) {
if ( ! $this->alternate_indexer ) {
$this->indexing = true;
$hash = sanitize_text_field( $indexnonce );
searchwp_delete_option( 'indexnonce' );
do_action( 'searchwp_log', 'Performing background index ' . $hash );
if ( ! $busy ) {
searchwp_update_option( 'busy', true );
do_action( 'searchwp_log', 'OK to index, proceed' );
new SearchWPIndexer( $hash );
} else {
do_action( 'searchwp_log', '!!! Indexer BUSY !!!' );
}
exit;
}
} elseif ( $this->indexing ) {
do_action( 'searchwp_log', 'SHORT CIRCUIT: index process already running' );
}
$okay_to_delta = true;
if ( isset( $_REQUEST['swppurge'] ) && $purge_transient !== $purge_nonce ) {
do_action( 'searchwp_log', 'Invalid delta update request nonce' );
do_action( 'searchwp_log', ' Nonce: ' . $purge_nonce );
do_action( 'searchwp_log', ' Local: ' . $purge_transient );
if ( is_array( $toPurge ) && ! empty( $toPurge ) ) {
$okay_to_delta = false;
do_action( 'searchwp_log', 'However, local delta queue passes check' );
do_action( 'searchwp_log', 'Purge queue (' . count( $toPurge ) . '): ' . implode( ', ', $toPurge ) );
}
}
$processing_purge_queue = absint( get_option( SEARCHWP_PREFIX . 'processing_purge_queue' ) );
// check to see if we need to process a purgeQueue
if (
is_array( $toPurge )
&& ! empty( $toPurge )
&& empty( $processing_purge_queue )
&& searchwp_get_setting( 'initial_index_built' )
&& ! searchwp_wp_doing_ajax() // We don't want purge requests to run during other AJAX processes
) {
do_action( 'searchwp_log', 'Delta update request: ' . implode( ', ', $toPurge ) );
if ( apply_filters( 'searchwp_background_deltas', true ) ) {
// proceed with delta update
do_action( 'searchwp_log', 'Automatic delta index update' );
$doing_delta = searchwp_get_option( 'doing_delta' );
if ( ! $doing_delta ) {
// prevent delta update cycle by maxing out the number of attempts
$delta_attempts = absint( searchwp_get_option( 'delta_attempts' ) );
if ( $delta_attempts > apply_filters( 'searchwp_max_delta_attempts', 5 ) ) {
do_action( 'searchwp_log', 'TOO MANY DELTA ATTEMPTS, ABORT' );
return;
}
searchwp_update_option( 'delta_attempts', $delta_attempts + 1 );
// at this point we're viewing the screen that loads after making an edit, so we can't die();
searchwp_update_option( 'doing_delta', true );
$invalid_purge_requests = searchwp_get_option( 'purge_queue_req' );
$invalid_purge_requests = absint( $invalid_purge_requests );
if ( $okay_to_delta ) {
if ( ! empty( $invalid_purge_requests ) ) {
do_action( 'searchwp_log', 'Resetting delta request count' );
searchwp_update_option( 'purge_queue_req', 0 );
}
$purge_queue_count = $processing_purge_queue + 1;
do_action( 'searchwp_log', 'Updating purge queue status: ' . (string) $purge_queue_count );
update_option( SEARCHWP_PREFIX . 'processing_purge_queue', $purge_queue_count, 'no' );
$this->process_updates();
} else {
$new_purge_request_count = $invalid_purge_requests + 1;
searchwp_update_option( 'purge_queue_req', $new_purge_request_count );
do_action( 'searchwp_log', '!! Breaking out of delta update to prevent cycling (' . $new_purge_request_count . ')' );
}
}
} else {
do_action( 'searchwp_log', 'Background delta index update prevented' );
}
} else {
// TODO: this is potentially never reached
if ( ! empty( $processing_purge_queue ) ) {
do_action( 'searchwp_log', 'Cleaning up after processing purge queue' );
update_option( SEARCHWP_PREFIX . 'processing_purge_queue', 0, 'no' );
searchwp_update_option( 'delta_attempts', 0 );
}
}
}
/**
* Perform the delta index updates based on what's changed (the purge queue)
*
* @since 1.6
*/
function process_updates( $conclude = false ) {
$processing_purge_queue = absint( get_option( SEARCHWP_PREFIX . 'processing_purge_queue' ) );
if ( $processing_purge_queue > 1 ) {
do_action( 'searchwp_log', 'process_updates() CANCELLED, indexer busy' );
return;
}
do_action( 'searchwp_log', 'process_updates() [' . $processing_purge_queue . ']' );
$hash = sprintf( '%.22F', microtime( true ) ); // inspired by $doing_wp_cron
update_option( 'swppurge_transient', $hash, 'no' );
$destination = esc_url( $this->endpoint . '?swppurge=' . $hash );
do_action( 'searchwp_log', 'Deferred purge ' . $destination );
// fire off our background request
$timeout = abs( apply_filters( 'searchwp_timeout', 0.02 ) );
$args = array(
'body' => array( 'swppurge' => $hash ),
'blocking' => false,
'user-agent' => 'SearchWP',
'timeout' => $timeout,
'sslverify' => false,
);
$args = apply_filters( 'searchwp_indexer_loopback_args', $args );
wp_remote_post( $destination, $args );
if ( ! empty( $conclude ) ) {
do_action( 'searchwp_log', 'process_updates() has terminated the page load' );
die();
}
}
/**
* Perform an indexer communication test and return the result
* @since 2.3.4
*/
function get_indexer_communication_result() {
if ( $this->alternate_indexer ) {
return;
}
$destination = esc_url( $this->endpoint );
$args = array(
'blocking' => true,
'user-agent' => 'SearchWP',
'timeout' => 5,
'sslverify' => false,
);
$args = apply_filters( 'searchwp_indexer_com_check_args', $args );
$response = wp_remote_post( $destination, $args );
$this->indexer_http_response_handler( $response );
return $response;
}
/**
* Handle various outcomes from wp_remote_post() calls
* @since 2.3.4
*
* @param $response
*/
function indexer_http_response_handler( $response ) {
if ( is_wp_error( $response ) ) {
return;
}
$existing_settings = array(
'basic_auth' => searchwp_get_setting( 'basic_auth' ),
);
// check for HTTP Basic Auth that has changed over time
if ( isset( $response['response']['code'] ) && 401 === (int) $response['response']['code'] ) {
// HTTP Basic Auth is enabled
if ( true !== $existing_settings['basic_auth'] ) {
// SearchWP previously detected that it was not enabled, so flag it as enabled because it is now
searchwp_set_setting( 'basic_auth', true );
}
} elseif ( isset( $response['response']['code'] ) && 401 !== (int) $response['response']['code'] && true == $existing_settings['basic_auth'] ) {
// SearchWP has the environment flagged as using HTTP Basic Auth, but that is no longer the case
searchwp_set_setting( 'basic_auth', 'no' );
}
}
/**
* Checks to make sure the proper database tables exist
*
* @since 1.2.3
*/
function validate_database_environment() {
do_action( 'searchwp_log', 'validate_database_environment()' );
$this->validDatabaseEnvironment = $this->custom_db_tables_exist();
searchwp_set_setting( 'valid_db_environment', $this->validDatabaseEnvironment );
}
/**
* Checks whether every necessary custom database table exists
*
* @since 2.5
* @return bool
*/
function custom_db_tables_exist() {
global $wpdb;
$tables_exist = true;
foreach ( $this->tables as $tableKey => $tableMeta ) {
$tableName = $wpdb->prepare( '%s', $wpdb->prefix . SEARCHWP_DBPREFIX . $tableMeta['table'] );
$tableSQL = $wpdb->get_results( "SHOW TABLES LIKE {$tableName}" , ARRAY_N );
if ( ! empty( $tableSQL ) ) {
$this->tables[ $tableKey ]['exists'] = true;
} else {
$tables_exist = false;
}
}
return $tables_exist;
}
/**
* Outputs an upgrade notice on the Plugins page
*
* @param $plugin_file
* @param $plugin_data
* @param $status
*
* @since 1.0
*/
function plugin_row( $plugin_file = null, $plugin_data, $status = null ) {
if ( ! is_null( $plugin_file ) ) {
$plugin_file = null;
}
if ( ! is_null( $status ) ) {
$status = null;
}
do_action( 'searchwp_log', 'plugin_row()' );
?>
<tr class="plugin-update-tr searchwp">
<td colspan="3" class="plugin-update">
<div class="update-message">
<?php _e( 'SearchWP must be updated to the latest version to work with ', 'searchwp' ); ?> <?php echo esc_html( $plugin_data['Name'] ); ?>
</div>
</td>
</tr>
<?php
}
/**
* Set up and trigger background index call
*/
function trigger_index() {
if ( ! $this->alternate_indexer ) {
$hash = sprintf( '%.22F', microtime( true ) ); // inspired by $doing_wp_cron
update_option( 'searchwp_transient', $hash, 'no' );
$destination = esc_url( $this->endpoint . '?swpnonce=' . $hash );
do_action( 'searchwp_log', 'trigger_index() ' . $destination );
$timeout = abs( apply_filters( 'searchwp_timeout', 0.02 ) );
$args = array(
'body' => array( 'swpnonce' => $hash ),
'blocking' => true,
'user-agent' => 'SearchWP',
'timeout' => $timeout,
'sslverify' => false,
);
$args = apply_filters( 'searchwp_indexer_loopback_args', $args );
$response = wp_remote_post( $destination, $args );
// $response will always be an error because of the timeout, but if we get error 6
// the issue is likely that the server rejected the loopback, so we'll check for that.
$screen = ! searchwp_wp_doing_ajax() && function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if (
did_action( 'searchwp_settings_init' ) // Only show on main settings screen.
&& (
is_object( $screen )
&& isset( $screen->base )
&& 'settings_page_searchwp' === $screen->base
)
&& $this->is_loopback_error( $response )
) {
$this->nags_utils->loopback_failure_nag();
}
} else {
$this->trigger_forced_indexer_chunk();
// if the chunk size is less than the number of outstanding posts
// we will need to rely on the cron job to pick up the other edits
do_action( 'searchwp_log', 'Triggering indexer (alternate indexer)' );
}
}
/**
* Detect whether a response contains information indicating a loopback error has occurred.
*
* @param mixed $error The variable to inspect.
*
* @since 3.1
*
* @return bool Whether the submitted variable is a loopback error.
*/
public function is_loopback_error( $error ) {
if ( ! is_wp_error( $error ) ) {
return false;
}
if ( ! is_array( $error->errors ) ) {
return false;
}
if ( ! array_key_exists( 'http_request_failed', $error->errors ) ) {
return false;
}
if ( ! isset( $error->errors['http_request_failed'][0] ) ) {
return false;
}
$error_message = $error->errors['http_request_failed'][0];
// Our best guess that this was a loopback error is the cURL error that's returned.
if ( false === strpos( strtolower( $error_message ), 'curl error 6: could not resolve' ) ) {
return false;
}
do_action( 'searchwp_log', 'Loopback failure detected! ' . $error_message );
return true;
}
/**
* Checks to see if we're in the main query and stores result as isMainQuery property
*
* @param WP_Query $query Instance of WP_Query to check
* @return mixed $query
*
* @since 1.0
*/
function check_for_main_query( $query ) {
if ( $query->is_main_query() || $this->force_run ) {
if ( ! isset( $_GET['swpjumpstart'] ) ) {
// do_action( 'searchwp_log', 'check_for_main_query(): It is the main query' );
}
$this->isMainQuery = true;
} else {
$this->isMainQuery = false;
}
return $query;
}
/**
* By default SearchWP short circuits on empty search queries, but the engine configuration
* itself insofar as exclusion/inclusion should still take place here
*
* @since 2.8.5
*
* @param $query
*/
function impose_engine_config( $query ) {
if ( is_admin() || ! $query->is_search || ! isset( $_REQUEST['s'] ) || ( isset( $_REQUEST['s'] ) && strlen( trim( $_REQUEST['s'] ) ) > 0 ) ) {
// this only applies if it's an empty search outside the admin (which has SearchWP short circuit)
return;
}
if ( apply_filters( 'searchwp_disable_impose_engine_config', false ) ) {
return;
}
$searchwp_search = new SearchWPSearch();
$search_terms = explode( ' ', get_search_query() );
$search_terms = array_map( 'sanitize_text_field', $search_terms );
$search_terms = array_map( 'trim', $search_terms );
$searchwp_settings = $this->settings['engines']['default'];
$post_types = array();
$excluded = array();
$included = array();
foreach ( $searchwp_settings as $post_type => $post_type_settings ) {
// enable the post type
if ( ! empty( $post_type_settings['enabled'] ) ) {
$post_types[] = $post_type;
}
// check for excluded
$excluded = apply_filters( 'searchwp_exclude', $searchwp_search->get_excluded(), 'default', $search_terms );
if ( is_array( $excluded ) ) {
$excluded = array_map( 'absint', $excluded );
} else {
$excluded = array();
}
// check for included
$included = apply_filters( 'searchwp_include', $searchwp_search->get_included(), 'default', $search_terms );
if ( is_array( $included ) ) {
$included = array_map( 'absint', $included );
} else {
$included = array();
}
}
// force post type limit
$query->set( 'post_type', $post_types );
// force excluded IDs
if ( ! empty( $excluded ) ) {
$query->set( 'post__not_in', $excluded );
}
if ( ! empty( $included ) ) {
$query->set( 'post__in', $included );
}
}
/**
* Perform a search query
*
* @param string $engine The search engine name to use when performing the search
* @param $terms string|array The search terms to include in the query
* @param int $page Results are paged, return this page (1 based)
*
* @return array Search results post IDs ordered by weight DESC
* @uses SearchWPSearch
* @since 1.0
*/
function search( $engine = 'default', $terms, $page = 1 ) {
do_action( 'searchwp_log', 'search()' );
$this->active = true;
$this->ran = true;
// at the very least, our terms are the search query
$terms = $this->original_query = is_array( $terms ) ? urldecode( htmlspecialchars_decode( trim( implode( ' ', $terms ) ) ) ) : urldecode( htmlspecialchars_decode( trim( (string) $terms ) ) );
// facilitate filtering the actual terms
$terms = apply_filters( 'searchwp_terms', $terms, $engine );
do_action( 'searchwp_log', '$terms after searchwp_terms = ' . var_export( $terms, true ) );
// handle sanitization — this filter is also applied in the SearchWPSearch class constructor
$sanitizeTerms = apply_filters( 'searchwp_sanitize_terms', true, $engine );
if ( ! is_bool( $sanitizeTerms ) ) {
$sanitizeTerms = true;
}
// whitelist search terms
$pre_whitelist_terms = is_array( $terms ) ? implode( ' ', $terms ) : ' ' . $terms . ' ';
$whitelisted_terms = $this->extract_terms_using_pattern_whitelist( $pre_whitelist_terms, false );
$apply_rules_to_whitelist = apply_filters( 'searchwp_apply_rules_to_whitelisted_terms', true );
if ( $apply_rules_to_whitelist ) {
$whitelisted_terms = $this->apply_rules_to_terms( $whitelisted_terms, $engine );
}
if ( apply_filters( 'searchwp_exclusive_regex_matches', false ) && ! empty( $whitelisted_terms ) ) {
$terms = $this->process_exclusive_regex_matches( $terms, $whitelisted_terms );
}
// if we should still sanitize our terms, do it
if ( $sanitizeTerms ) {
$terms = $this->sanitize_terms( $terms, $engine );
}
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_unique( array_merge( $terms, $whitelisted_terms ) );
} else {
$terms .= ' ' . implode( ' ', $whitelisted_terms );
}
do_action( 'searchwp_log', '$sanitizeTerms = ' . print_r( $sanitizeTerms, true ) );
do_action( 'searchwp_log', '$terms = ' . print_r( $terms, true ) );
// set up our engine name
if ( ! $this->is_valid_engine( $engine ) ) {
/** @noinspection PhpUndefinedClassInspection */
return new WP_Error( 'searchwp_invalid_engine', __( 'Invalid SearchWP Engine: ' . $engine, 'searchwp' ) );
}
do_action( 'searchwp_log', '$engine = ' . $engine );
// make sure the search isn't overflowing with terms
$maxSearchTerms = intval( apply_filters( 'searchwp_max_search_terms', 6, $engine ) );
do_action( 'searchwp_log', 'searchwp_max_search_terms $maxSearchTerms = ' . $maxSearchTerms );
$maxSearchTerms = intval( apply_filters( 'searchwp_max_search_terms_supplemental', $maxSearchTerms, $engine ) );
do_action( 'searchwp_log', 'searchwp_max_search_terms_supplemental $maxSearchTerms = ' . $maxSearchTerms );
$maxSearchTerms = intval( apply_filters( "searchwp_max_search_terms_{$engine}", $maxSearchTerms ) );
do_action( 'searchwp_log', 'searchwp_max_search_terms_{$engine} $maxSearchTerms = ' . $maxSearchTerms );
if ( count( $terms ) > $maxSearchTerms ) {
$terms = array_slice( $terms, 0, $maxSearchTerms );
do_action( 'searchwp_log', '$terms = ' . print_r( $terms, true ) );
$this->register_search_query_modification( 'max_terms' );
} else {
do_action( 'searchwp_log', 'Terms within max search terms' );
}
// prep our args
$args = array(
'engine' => $engine,
'terms' => $terms,
'page' => intval( $page ),
'posts_per_page' => apply_filters( 'searchwp_posts_per_page', intval( get_option( 'posts_per_page' ) ), $engine, $terms, $page )
);
do_action( 'searchwp_log', '$args = ' . print_r( $args, true ) );
// perform the search
$profiler = array( 'before' => microtime() );
$searchwp = $this->do_search( $args );
$profiler['after'] = microtime();
$this->foundPosts = intval( $searchwp->foundPosts );
$this->maxNumPages = intval( $searchwp->maxNumPages );
$this->results_weights = $searchwp->results_weights;
$this->search_sql = $searchwp->get_sql();
// store diagnostics for debugging
$this->diagnostics[] = array(
'engine' => $args['engine'],
'terms' => $args['terms'],
'found_posts' => $searchwp->foundPosts,
'posts' => $searchwp->posts,
'profiler' => $profiler,
'args' => $args,
);
$this->active = false;
$results = apply_filters( 'searchwp_results', $searchwp->posts, array(
'terms' => $terms,
'page' => $args['page'],
'order' => 'DESC',
'foundPosts' => $this->foundPosts,
'maxNumPages' => $this->maxNumPages,
'engine' => $engine,
) );
// Allow devs ability to re-count results if they choose to manipulate them.
$this->foundPosts = apply_filters( 'searchwp_results_found_posts', $this->foundPosts, $engine );
$this->maxNumPages = apply_filters( 'searchwp_results_max_num_pages', $this->maxNumPages, $engine );
do_action( 'searchwp_log', '$this->foundPosts = ' . $this->foundPosts );
do_action( 'searchwp_log', '$this->maxNumPages = ' . $this->maxNumPages );
return $results;
}
/**
* When debugging is enabled, append the result weight to the title for reference
*
* @param $posts
* @param $search_args
*
* @return mixed
*/
function maybe_append_weight_to_result_title( $posts, /** @noinspection PhpUnusedParameterInspection */ $search_args ) {
if (
apply_filters( 'searchwp_debug', false )
&& apply_filters( 'searchwp_debug_append_weights_to_titles', false )
&& current_user_can( $this->settings_cap )
&& ! empty( $posts )
) {
// the search just ran so we can reference the result weights
$weights = $this->results_weights;
foreach ( $posts as $key => $val ) {
if ( array_key_exists( $posts[ $key ]->ID, $weights ) ) {
$posts[ $key ]->post_title .= ' - ' . absint( $weights[ $posts[ $key ]->ID ]['weight'] );
}
}
}
return $posts;
}
/**
* Return the last stored search SQL
*
* @since 2.6
*
* @return string Last stored search query
*/
function get_last_search_sql() {
return $this->search_sql;
}
public function validate_terms_in_index_by_id( $term_ids, $engine = 'default' ) {
global $wpdb;
// There's a chance this engine doesn't exist. If the engine doesn't exist
// instead of assuming the term is invalid, we're going to assume it IS valid
// because term validation is new as of version 3.1 and there is other code
// that can end up here and break because we're invalidating what was once valid.
if ( ! SWP()->is_valid_engine( $engine ) ) {
return $term_ids;
}
$hash = md5( $engine . json_encode( $term_ids ) );
if ( array_key_exists( $hash, $this->validated_terms ) ) {
return $this->validated_terms[ $hash ];
}
$term_ids = array_map( 'absint', $term_ids );
$algorithm = new SearchWPSearch();
$post_status_clause = $algorithm->post_status_limiter_sql( SWP()->settings['engines'][ $engine ] );
$swp_db_prefix = $wpdb->prefix . SEARCHWP_DBPREFIX;
$placeholders = implode( ', ', array_fill( 0, count( $term_ids ), '%d' ) );
$main_table_present = $wpdb->get_col(
$wpdb->prepare(
"SELECT term
FROM {$swp_db_prefix}index
LEFT JOIN {$wpdb->posts} ON {$wpdb->posts}.ID = {$swp_db_prefix}index.post_id
WHERE term IN ( $placeholders )
{$post_status_clause}
GROUP BY term",
$term_ids
)
);
$meta_table_present = $wpdb->get_col(
$wpdb->prepare(
"SELECT term
FROM {$swp_db_prefix}cf
LEFT JOIN {$wpdb->posts} ON {$wpdb->posts}.ID = {$swp_db_prefix}cf.post_id
WHERE term IN ( $placeholders )
{$post_status_clause}",
$term_ids
)
);
$tax_table_present = $wpdb->get_col(
$wpdb->prepare(
"SELECT term
FROM {$swp_db_prefix}tax
LEFT JOIN {$wpdb->posts} ON {$wpdb->posts}.ID = {$swp_db_prefix}tax.post_id
WHERE term IN ( $placeholders )
{$post_status_clause}",
$term_ids
)
);
$valid_term_ids = array_merge(
$main_table_present,
$meta_table_present,
$tax_table_present
);
$valid_term_ids = array_map( 'absint', $valid_term_ids );
$valid_term_ids = array_unique( $valid_term_ids );
$this->validated_terms[ $hash ] = $valid_term_ids;
return $valid_term_ids;
}
/**
* Determines whether the submitted term ID is present in the index given the submitted engine.
*
* @since 3.1
*/
public function validate_term_in_index_by_id( $term_id, $engine ) {
global $wpdb;
// There's a chance this engine doesn't exist. If the engine doesn't exist
// instead of assuming the term is invalid, we're going to assume it IS valid
// because term validation is new as of version 3.1 and there is other code
// that can end up here and break because we're invalidating what was once valid.
if ( ! SWP()->is_valid_engine( $engine ) ) {
return true;
}
$term_id = absint( $term_id );
$algorithm = new SearchWPSearch();
$post_status_clause = $algorithm->post_status_limiter_sql( SWP()->settings['engines'][ $engine ] );
$swp_db_prefix = $wpdb->prefix . SEARCHWP_DBPREFIX;
$main_table_present = $wpdb->get_row(
$wpdb->prepare(
"SELECT *
FROM {$swp_db_prefix}index
LEFT JOIN {$wpdb->posts} ON {$wpdb->posts}.ID = {$swp_db_prefix}index.post_id
WHERE term = %d
{$post_status_clause}
LIMIT 1",
$term_id
)
);
if ( empty( $main_table_present ) ) {
$meta_table_present = $wpdb->get_row(
$wpdb->prepare(
"SELECT *
FROM {$swp_db_prefix}cf
LEFT JOIN {$wpdb->posts} ON {$wpdb->posts}.ID = {$swp_db_prefix}cf.post_id
WHERE term = %d
{$post_status_clause}
LIMIT 1",
$term_id
)
);
}
if ( empty( $main_table_present ) && empty( $meta_table_present ) ) {
$tax_table_present = $wpdb->get_row(
$wpdb->prepare(
"SELECT *
FROM {$swp_db_prefix}tax
LEFT JOIN {$wpdb->posts} ON {$wpdb->posts}.ID = {$swp_db_prefix}tax.post_id
WHERE term = %d
{$post_status_clause}
LIMIT 1",
$term_id
)
);
}
return $main_table_present || $meta_table_present || $tax_table_present;
}
/**
* Determines if an engine name is considered valid (e.g. stored in the settings)
*
* @param $engineName string The engine name to check
*
* @return bool
*/
public function is_valid_engine( $engineName ) {
$engineName = sanitize_key( $engineName );
$validEngine = is_string( $engineName ) && isset( $this->settings['engines'][ $engineName ] );
do_action( 'searchwp_log', 'is_valid_engine( ' . print_r( $engineName, true ) . ' ) = ' . var_export( $validEngine, true ) );
return $validEngine;
}
/**
* Enforce 4-byte UTF-8 when utf8mb4 is not supported
*
* @since 2.5.7
* @link http://stackoverflow.com/questions/16496554/can-php-detect-4-byte-encoded-utf8-chars
*
* @param $string
*
* @return mixed
*/
function replace_4_byte( $string ) {
return preg_replace( '%(?:
\xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
)%xs', '', $string );
}
/**
* Removes punctuation
* @param $termString string The dirty string
*
* @return string The cleaned string
*/
public function clean_term_string( $termString ) {
$punctuation = array( '(', ')', '{', '}', '[', ']', '·', "'", '´', '’', '‘', '”', '“', '„', '—', '–', '×', '…', '€', "\n", '.', ',', ';', '`' );
if ( ! is_string( $termString ) ) {
$termString = '';
}
$termString = trim( $termString );
if ( function_exists( 'mb_convert_encoding' ) ) {
$termString = mb_convert_encoding( $termString, 'UTF-8', 'UTF-8' );
}
if ( empty( $this->settings['utf8mb4'] ) ) {
$termString = $this->replace_4_byte( $termString );
}
$termString = function_exists( 'mb_strtolower' ) ? mb_strtolower( $termString, 'UTF-8' ) : strtolower( $termString );
$termString = stripslashes( $termString );
// remove punctuation
$termString = str_replace( $punctuation, ' ', $termString );
$termString = preg_replace( '/[[:punct:]]/uiU', ' ', $termString );
$termString = preg_replace( '/[\x00-\x1F\x7F\xA0]/u', ' ', $termString );
$termString = preg_replace( '/[\x{0300}-\x{036f}]+/u', ' ', $termString );
$termString = preg_replace( '/[^\P{C}\n]+/u', ' ', $termString );
// remove spaces
$termString = preg_replace( '/[[:space:]]/uiU', ' ', $termString );
// final pass
$termString = sanitize_text_field( $termString );
$termString = trim( $termString );
return $termString;
}
/**
* Sanitizes terms; should be trimmed, single words.
*
* @param $terms string|array The terms to sanitize
*
* @param string $engine
*
* @return array Valid terms
*/
public function sanitize_terms( $terms, $engine = 'default' ) {
$validTerms = array();
// always going to be a string when a search query is performed
if ( is_string( $terms ) ) {
$whitelisted_terms = $this->extract_terms_using_pattern_whitelist( $terms );
// maybe remove matches so we don't have redundancy, they were buffered with spaces to ensure whole word matching instead of partial matching
if ( ! empty( $whitelisted_terms ) ) {
$terms = str_ireplace( $whitelisted_terms, '', $terms );
}
// clean up the double space flag we used
$terms = str_replace( ' ', ' ', $terms );
// process the (potentially stripped of whitelist matches) string to strip out unwanted punctuation
$terms = $this->clean_term_string( trim( $terms ) );
// put the terms in an array
$terms = ( strpos( $terms, ' ' ) !== false ) ? explode( ' ', $terms ) : array( $terms );
// after extracting whitelist matches there might be some empty keys
$terms = array_map( 'trim', $terms );
$terms = array_filter( $terms, 'strlen' );
// maybe prepend our whitelisted terms to the final term array
if ( ! empty( $whitelisted_terms ) && is_array( $whitelisted_terms ) ) {
$whitelisted_terms = array_map( 'trim', $whitelisted_terms );
$whitelisted_terms = array_filter( $whitelisted_terms, 'strlen' );
$terms = array_merge( $whitelisted_terms, $terms );
}
}
if ( is_array( $terms ) ) {
// loop through each term, check it against the whitelist, and ensure it meets all criteria to be considered valid
foreach ( $terms as $key => $term ) {
$whitelist_match_check = ' ' . $term . ' ';
// first check the term for a whitelist match
$whitelist_matches = $this->extract_terms_using_pattern_whitelist( $term );
$apply_rules_to_whitelist = apply_filters( 'searchwp_apply_rules_to_whitelisted_terms', true );
if ( $apply_rules_to_whitelist ) {
$whitelist_matches = $this->apply_rules_to_terms( $whitelist_matches, $engine );
}
if ( ! empty( $whitelist_matches ) ) {
// if there were matches (but it wasn't a complete match) append it to the array for further processing
$whitelist_extraction_result = str_ireplace( $whitelist_matches, '', $whitelist_match_check );
// remove the buffer used for full matching
if ( is_array( $whitelist_matches ) ) {
$whitelist_matches = array_map( 'trim', $whitelist_matches );
$whitelist_matches = array_filter( $whitelist_matches, 'strlen' );
}
if ( strlen( trim( $whitelist_extraction_result ) ) > 0 ) { // had extra whitespace
// it was not an exact match so we need to clean what did not match
$whitelist_extraction_result = $this->clean_term_string( $whitelist_extraction_result );
// check for spaces in what was left over
if ( strpos( $whitelist_extraction_result, ' ' ) ) {
// append the (now separated) terms and the whitelist match(es) to the terms array and essentially short circuit this pass
$terms = array_merge( $terms, $whitelist_matches, explode( ' ', $whitelist_extraction_result ) );
} else {
// append the term and the whitelist match(es) to the terms array and essentially short circuit this pass
$terms = array_merge( $terms, $whitelist_matches, explode( ' ', $whitelist_extraction_result ) );
}
} else {
// it was an exact match to a pattern in the whitelist, so add the term(s) as-is
if ( 1 == count( $whitelist_matches ) ) {
// it was a single match, add as-is to what are considered valid terms
$validTerms[ $key ] = sanitize_text_field( trim( $whitelist_matches[0] ) );
} else {
// append all the matches to this array; they should eventually match exactly
$terms = array_merge( $terms, $whitelist_matches );
}
}
} else {
// no whitelist match
$term = $this->clean_term_string( $term );
if ( strpos( $term, ' ' ) ) {
// append the new broken down terms
$terms = array_merge( $terms, explode( ' ', $term ) );
} else {
// proceed
$excludeCommon = apply_filters_deprecated( 'searchwp_exclude_common', array( true ), '3.0', 'searchwp_exclude_stopwords' );
$excludeCommon = apply_filters( 'searchwp_exclude_stopwords', $excludeCommon );
if ( ! is_bool( $excludeCommon ) ) {
$excludeCommon = true;
}
$common_words_for_engine = apply_filters_deprecated( "searchwp_common_words_{$engine}", array( $this->common ), '3.0', "searchwp_stopwords_{$engine}" );
$common_words_for_engine = (array) apply_filters( "searchwp_stopwords_{$engine}", $common_words_for_engine );
if ( ( $excludeCommon && ! in_array( $term, $common_words_for_engine ) ) || ! $excludeCommon ) {
$minLength = absint( apply_filters( 'searchwp_minimum_word_length', 3 ) );
if ( $minLength <= strlen( $term ) ) {
$validTerms[ $key ] = sanitize_text_field( trim( $term ) );
} else {
$this->register_search_query_modification( 'min_word_length' );
}
} else {
$this->register_search_query_modification( 'stopword' );
}
}
}
}
}
// after removing punctuation we might have some empty keys
$validTerms = array_filter( $validTerms, 'strlen' );
// we also might have duplicates
$validTerms = array_values( array_unique( $validTerms ) );
return $validTerms;
}
function apply_rules_to_terms( $terms, $engine = 'default' ) {
$excludeCommon = apply_filters_deprecated( 'searchwp_exclude_common', array( true ), '3.0', 'searchwp_exclude_stopwords' );
$excludeCommon = apply_filters( 'searchwp_exclude_stopwords', $excludeCommon );
if ( ! is_bool( $excludeCommon ) ) {
$excludeCommon = true;
}
$common_words_for_engine = apply_filters_deprecated( "searchwp_common_words_{$engine}", array( $this->common ), '3.0', "searchwp_stopwords_{$engine}" );
$common_words_for_engine = apply_filters( "searchwp_stopwords_{$engine}", $common_words_for_engine );
$minLength = absint( apply_filters( 'searchwp_minimum_word_length', 3 ) );
foreach ( (array) $terms as $key => $term ) {
// Check for minimum word length.
if ( strlen( $term ) < $minLength ) {
$this->register_search_query_modification( 'min_word_length' );
unset( $terms[ $key ] );
}
// Check for stopword.
if ( $excludeCommon && in_array( $term, $common_words_for_engine ) ) {
$this->register_search_query_modification( 'stopword' );
unset( $terms[ $key ] );
}
}
return array_values( $terms );
}
/**
* Determine whether to short circuit SearchWP by circumstance
*
* @sine 2.6.1
* @return bool whether to short circuit
*/
function maybe_short_circuit() {
global $wp_query;
if ( empty( $wp_query ) ) {
do_action( 'searchwp_log', 'Short circuiting [1]' );
return true;
}
// If there are no initial settings, we can short circuit
$initial_settings = searchwp_get_setting( 'initial_settings' );
if ( empty( $initial_settings ) ) {
do_action( 'searchwp_log', 'Short circuiting [2]' );
return true;
}
// Admin search but the current post type is not added to the admin engine.
if (
! empty( $_REQUEST['s'] )
&& is_admin()
&& searchwp_get_setting_advanced( 'admin_search' ) // Admin search enabled?
&& ! $this->is_applicable_admin_search()
) {
if ( ! searchwp_wp_doing_ajax() ) {
$this->nags_utils->admin_search_post_type_nag();
}
return true;
}
// for native searches SearchWP should short-circuit on empty searches so as to match WP default behavior
// (SearchWP will return no results on empty searches, WordPress returns everything)
$return = is_search()
&& $this->isMainQuery
&& (
isset( $_REQUEST['s'] )
&& 0 === strlen( trim( urldecode( $_REQUEST['s'] ) ) )
);
if ( $return ) {
do_action( 'searchwp_log', 'Short circuiting [3]' );
}
return $return;
}
public function is_applicable_admin_search() {
if ( ! is_admin() ) {
return false;
}
if ( ! class_exists( 'WP_Screen' ) || ! function_exists( 'get_current_screen') ) {
return false;
}
$current_screen = get_current_screen();
if ( ! $current_screen instanceof WP_Screen ) {
return false;
}
$post_type = null;
if ( isset( $current_screen->id ) ) {
if ( 'upload' == $current_screen->id ) {
$post_type = 'attachment';
} elseif ( isset( $current_screen->post_type ) ) {
$post_type = sanitize_text_field( $current_screen->post_type );
}
}
if ( is_null( $post_type ) ) {
return false;
}
$admin_engine = $this->get_admin_engine();
if ( empty( $admin_engine ) ) {
return false;
}
if ( ! $this->is_post_type_enabled_in_engine( $post_type, $admin_engine ) ) {
return false;
}
return true;
}
/**
* Prevent WordPress from performing it's own search database call
*
* @param $query
*
* @return bool|string
* @since 1.1.2
*/
function maybe_cancel_wp_query( $sql, $query ) {
global $wpdb, $wp_query;
if ( empty( $query ) || empty( $wp_query ) ) {
return $sql;
}
$proceedIfInAdmin = apply_filters( 'searchwp_in_admin', false );
$overridden = apply_filters( 'searchwp_force_wp_query', false );
$shortCircuit = apply_filters( 'searchwp_short_circuit', $this->maybe_short_circuit(), $this );
if (
$this->force_run
|| (
! $shortCircuit
&& ! $overridden
&& ! (
is_admin()
&& ! $proceedIfInAdmin
)
&& ! is_feed()
&& is_search()
&& $query->is_main_query()
)
) {
// prevent the original search query from running with something that has the least impact
$sql = "SELECT * FROM $wpdb->posts WHERE 1=0";
do_action( 'searchwp_log', 'maybe_cancel_wp_query() canceled the query ' );
}
return $sql;
}
/**
* Conditionally trigger Admin Bar entry if SearchWP ran
*
* @since 2.5
*/
function admin_bar_entry_for_search() {
if ( is_admin_bar_showing() && $this->ran ) {
$this->admin_bar_search_results_assets(); // already in wp_footer so just fire it directly
add_action( 'wp_before_admin_bar_render', array( $this, 'admin_bar_search_results' ), 999 );
}
}
/**
* The assets for the Admin Bar entry
*
* @since 2.5
*/
function admin_bar_search_results_assets() {
?>
<style type="text/css">
#wpadminbar .searchwp-admin-bar-warning {
background-color:#b60 !important;
color:#fff !important;
}
</style>
<?php
}
/**
* Output an Admin Bar entry on search results pages that can be used
* for many things such as notification of search query modification
* that devs may not realize is happening
*
* @since 2.5
*/
function admin_bar_search_results() {
global $wp_admin_bar;
if ( ! apply_filters( 'searchwp_admin_bar', true ) ) {
return;
}
// as of now we're only going to show an Admin Bar entry if there
// were search query modifications, to help call out things like
// minimum word length and common words in a more effective way
if ( empty( $this->search_query_mods ) ) {
return;
}
$args = array(
'id' => 'searchwp-admin-bar-search-results',
'title' => 'SearchWP',
);
// conditional is redundant but put in place in case non-warning entries are added
if ( ! empty( $this->search_query_mods ) ) {
$args['meta'] = array( 'class' => 'searchwp-admin-bar-warning' );
}
if ( method_exists( $wp_admin_bar, 'add_menu' ) ) {
$wp_admin_bar->add_menu( $args );
if ( ! empty( $this->search_query_mods ) ) {
foreach ( $this->search_query_mods as $key => $search_query_modification ) {
// default args
$sub_menu_args = array(
'parent' => 'searchwp-admin-bar-search-results',
'id' => esc_attr( $key ),
);
// add in the search mod args (defined in register_search_query_modification())
$sub_menu_args = array_merge( $sub_menu_args, $search_query_modification );
$wp_admin_bar->add_menu( $sub_menu_args );
}
}
}
}
/**
* Keeps a running log of any modifications applied to a search query for use
* in the Admin Bar entry (call it out to developers)
*
* @since 2.5
* @param $modification
*/
private function register_search_query_modification( $modification ) {
$acceptable_mods = array(
'min_word_length' => array(
'title' => __( 'Minimum word length triggered', 'searchwp' ),
'href' => 'https://searchwp.com/?p=6598',
'meta' => array(
'class' => 'searchwp-admin-bar-warning',
),
),
'stopword' => array(
'title' => __( 'Stopword removed', 'searchwp' ),
'href' => 'https://searchwp.com/?p=163991',
'meta' => array(
'class' => 'searchwp-admin-bar-warning',
),
),
'max_terms' => array(
'title' => __( 'Search query length too long', 'searchwp' ),
'href' => 'https://searchwp.com/?p=6664',
'meta' => array(
'class' => 'searchwp-admin-bar-warning',
),
),
);
if ( ! array_key_exists( $modification, $acceptable_mods ) ) {
return;
}
// 'enqueue' this modification for output in the Admin Bar entry
if ( ! isset( $this->search_query_mods[ $modification ] ) ) {
$this->search_query_mods[ $modification ] = $acceptable_mods[ $modification ];
}
do_action( 'searchwp_log', 'Search query modification: ' . $acceptable_mods[ $modification ]['title'] . ' ' . $acceptable_mods[ $modification ]['href'] );
}
/**
* Retrieves the admin engine name
*
* @since 3.0.6
*
* @return string|boolean
*/
public function get_admin_engine() {
$advanced_options = searchwp_get_option( 'advanced' );
$advanced_engine = is_array( $advanced_options ) && array_key_exists( 'admin_engine', $advanced_options ) ? $advanced_options['admin_engine'] : '';
return $this->is_valid_engine( $advanced_engine ) ? $advanced_engine : false;
}
/**
* Determine whether a particular post type is enabled in a particular engine.
*
* @since 3.0.6
*
* @param string $post_type The post type.
* @param string $engine The engine.
*
* @return bool
*/
function is_post_type_enabled_in_engine( $post_type = 'post', $engine = 'default' ) {
if ( ! $this->is_valid_engine( $engine ) ) {
return false;
}
if ( ! is_array( $this->settings['engines'] ) || ! array_key_exists( $engine, $this->settings['engines'] ) ) {
return false;
}
$engine_settings = $this->settings['engines'][ $engine ];
if ( ! is_array( $engine_settings ) || ! array_key_exists( $post_type, $engine_settings ) ) {
return false;
}
return ! empty( $engine_settings[ $post_type ]['enabled'] );
}
/**
* Callback for Media Grid Admin searches
*
* @since 2.8
*
* @param $args
*
* @return array
*/
function maybe_admin_media_search( $args ) {
$in_admin = apply_filters( 'searchwp_in_admin', false );
$admin_engine = $this->get_admin_engine();
if ( empty( $in_admin ) || ! $this->is_post_type_enabled_in_engine( 'attachment', $admin_engine ) ) {
return $args;
}
if ( ! current_user_can( 'upload_files' ) ) {
wp_send_json_error();
}
if ( empty( $args['s'] ) ) {
return $args;
}
$query_args = array(
's' => sanitize_text_field( $args['s'] ),
'engine' => $admin_engine,
'post_type' => 'attachment',
'fields' => 'ids',
);
if ( ! empty( $args['posts_per_page'] ) ) {
$query_args['posts_per_page'] = absint( $args['posts_per_page'] );
}
if ( ! empty( $args['paged'] ) ) {
$query_args['page'] = absint( $args['paged'] );
}
$search_results = new SWP_Query( $query_args );
$args = array(
'post__in' => $search_results->posts,
'orderby' => 'post__in',
'post_type' => 'attachment',
'post_status' => 'inherit',
's' => '',
);
// Re-implement pagination
if ( ! empty( $query_args['posts_per_page'] ) ) {
$args['posts_per_page'] = absint( $query_args['posts_per_page'] );
}
if ( ! empty( $query_args['page'] ) && ! empty( $query_args['paged'] ) ) {
$args['paged'] = absint( $query_args['paged'] );
}
return $args;
}
/**
* Callback for the_posts filter. Hijacks WordPress searches and returns SearchWP results
*
* @param $posts array The original posts array from WordPress' query
*
* @return array The posts in the search results from SearchWP
* @uses SearchWPSearch
* @since 1.0
*/
function wp_search( $posts ) {
global $wp_query;
// On the front end SearchWP searches run before any output so if 'wp' fired we don't want to run anyway
if ( ! is_admin() && did_action( 'wp' ) ) {
return $posts;
}
// If SearchWP is already active, don't run
if ( $this->active ) {
return $posts;
}
// Allow developers to NOT use SearchWP if another plugin is using $_GET['s'] for specific functionality
if ( apply_filters( 'searchwp_short_circuit', $this->maybe_short_circuit(), $this ) ) {
do_action( 'searchwp_log', 'Short circuiting at this time' );
return $posts;
}
/**
* We need to check for the main query here, but the process of acurately checking
* for the main query has proven to be FULL of edge cases and that is why this is
* ridiculous.
*/
$do_we_run = false;
$wp_query_is_search = is_search();
if ( empty( $wp_query_is_search ) ) {
$wp_query_is_search = ! empty( $wp_query->is_search ) ? true : false;
}
if ( ( $wp_query_is_search || is_admin() && ! empty( $_REQUEST['s'] ) ) && // What's up, admin?
( (
$this->isMainQuery // This is _usually_ correct
|| ( ! $this->isMainQuery && is_main_query() ) // but sometimes it's not!
)
|| $this->force_run )
) {
$do_we_run = true;
}
// Short circuit if it's not the main query
if ( ! $do_we_run ) {
return $posts;
}
// TODO: This nested conditional nest has become crazy
if ( ! $this->force_run ) {
// make sure we're not in the admin, that we are searching, that it is the main query, and that SearchWP is not active
$proceedIfInAdmin = apply_filters( 'searchwp_in_admin', false );
if ( is_admin() && ! $proceedIfInAdmin ) {
return $posts;
} elseif ( is_admin() ) {
// we're going to reset as false to ensure that we have an applicable environment (e.g. no searching Users)
$proceedIfInAdmin = false;
// hijack the search engine settings to limit to the current post type when searching post types in the admin
if ( isset( $this->settings['engines'] ) && is_array( $this->settings['engines'] ) ) {
// find out what screen we're on
if ( class_exists( 'WP_Screen' ) && function_exists( 'get_current_screen') ) {
$current_screen = get_current_screen();
if ( $current_screen instanceof WP_Screen ) {
if ( isset( $current_screen->id ) ) {
if ( 'upload' == $current_screen->id ) {
// we want to search Media only
$limit_results_to_post_type = 'attachment';
} elseif ( isset( $current_screen->post_type ) && ! empty( $current_screen->post_type ) ) {
// we want to limit to the current post type
$limit_results_to_post_type = sanitize_text_field( $current_screen->post_type );
}
}
if ( isset( $limit_results_to_post_type ) ) {
// we have a valid post type and we're on a valid screen, so let's proceed
$proceedIfInAdmin = true;
// We need to verify that this post type is enabled in the admin search engine.
$admin_engine = $this->get_admin_engine();
if ( ! empty( $admin_engine ) && ! $this->is_post_type_enabled_in_engine( $limit_results_to_post_type, $admin_engine ) ) {
$proceedIfInAdmin = false;
$admin_engine = false;
// @since 3.1 the short circuit and nag is placed in maybe_short_circuit()
}
// Disable all post types except the one we're searching so as to not pollute the results set.
foreach ( $this->settings['engines'] as $engine_name => $engine_settings ) {
foreach ( $engine_settings as $engine_settings_post_type => $engine_settings_post_type_settings ) {
if ( $limit_results_to_post_type == $engine_settings_post_type && isset( $this->settings['engines'][ $engine_name ][ $engine_settings_post_type ] ) ) {
if ( isset( $this->settings['engines'][ $engine_name ][ $engine_settings_post_type ]['enabled'] ) && false === $this->settings['engines'][ $engine_name ][ $engine_settings_post_type ]['enabled'] ) {
$this->settings['engines'][ $engine_name ][ $engine_settings_post_type ]['enabled'] = true;
// Also prevent attribution else we'll have no results.
$this->settings['engines'][ $engine_name ][ $engine_settings_post_type ]['options']['attribute_to'] = '';
}
} else {
if ( isset( $this->settings['engines'][ $engine_name ][ $engine_settings_post_type ]['enabled'] ) && true === $this->settings['engines'][ $engine_name ][ $engine_settings_post_type ]['enabled'] ) {
$this->settings['engines'][ $engine_name ][ $engine_settings_post_type ]['enabled'] = false;
}
}
}
}
}
}
}
}
}
}
do_action( 'searchwp_log', 'Native WordPress search detected' );
// a search is currently taking place, let's provide some wicked better results
$this->active = true;
$this->ran = true;
$wp_query_paged = get_query_var( 'paged' );
$wpPaged = ( intval( $wp_query_paged ) > 0 ) ? intval( $wp_query_paged ) : 1;
do_action( 'searchwp_log', '$wpPaged = ' . $wpPaged );
// at the very least, our terms are the search query
$wp_query_s = urldecode( htmlspecialchars_decode( stripslashes( get_query_var( 's' ) ) ) );
if ( empty( $wp_query_s ) ) {
$wp_query_s = isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '';
}
$this->original_query = $wp_query_s;
$terms_trimmed = trim( $wp_query_s );
// This is redundant, but legacy because stripslashes() used to be called here which is late
$terms = $terms_trimmed;
do_action( 'searchwp_log', '$terms = ' . var_export( $terms, true ) );
// facilitate filtering the actual terms
$terms = apply_filters( 'searchwp_terms', $terms, 'default' );
do_action( 'searchwp_log', '$terms after searchwp_terms = ' . var_export( $terms, true ) );
// we always work with lowercase
$terms = function_exists( 'mb_strtolower' ) ? mb_strtolower( $terms, 'UTF-8' ) : strtolower( $terms );
// handle sanitization
$sanitizeTerms = apply_filters( 'searchwp_sanitize_terms', true, 'default' );
if ( ! is_bool( $sanitizeTerms ) ) {
$sanitizeTerms = true;
}
// whitelist search terms
$pre_whitelist_terms = is_array( $terms ) ? implode( ' ', $terms ) : ' ' . $terms . ' ';
$whitelisted_terms = $this->extract_terms_using_pattern_whitelist( $pre_whitelist_terms, false );
if ( apply_filters( 'searchwp_exclusive_regex_matches', false ) && ! empty( $whitelisted_terms ) ) {
$terms = $this->process_exclusive_regex_matches( $terms, $whitelisted_terms );
}
// if we should still sanitize our terms, do it
if ( $sanitizeTerms ) {
$terms = $this->sanitize_terms( $terms );
}
if ( is_array( $whitelisted_terms ) ) {
$whitelisted_terms = array_filter( array_map( 'trim', $whitelisted_terms ), 'strlen' );
$apply_rules_to_whitelist = apply_filters( 'searchwp_apply_rules_to_whitelisted_terms', true );
if ( $apply_rules_to_whitelist ) {
$whitelisted_terms = $this->apply_rules_to_terms( $whitelisted_terms, 'default' );
}
}
if ( is_array( $terms ) ) {
$terms = array_filter( array_map( 'trim', $terms ), 'strlen' );
$terms = array_unique( array_merge( $terms, $whitelisted_terms ) );
} else {
$terms .= ' ' . implode( ' ', $whitelisted_terms );
}
do_action( 'searchwp_log', '$terms after sanitization = ' . var_export( $terms, true ) );
// determine the order from WP_Query
$wp_query_order = get_query_var( 'order' );
// Sometimes plugins or code will switch this to ASC but by default we want
// results sorted DESC by relevancy so we're going to assume that
$order = 'DESC';
if ( apply_filters( 'searchwp_force_wpquery_order', false ) ) {
$order = ( strtoupper( $wp_query_order ) == 'DESC' ) ? 'DESC' : 'ASC';
}
do_action( 'searchwp_log', '$order = ' . $order );
// make sure the search isn't overflowing with terms
$maxSearchTerms = intval( apply_filters( 'searchwp_max_search_terms', 6, 'default' ) );
do_action( 'searchwp_log', '$maxSearchTerms = ' . $maxSearchTerms );
if ( count( $terms ) > $maxSearchTerms ) {
$terms = array_slice( $terms, 0, $maxSearchTerms );
// need to tell $wp_query that we hijacked this
// EDIT: no we don't — we have an Admin Bar entry telling the dev it was changed
// $wp_query->query['s'] = $wp_query->query_vars['s'] = sanitize_text_field( implode( ' ', $terms ) );
$this->register_search_query_modification( 'max_terms' );
do_action( 'searchwp_log', 'Breached max terms count, $terms = ' . var_export( $terms, true ) );
}
if ( ! empty( $terms ) ) {
// get posts_per_page and offset from original $wp_query
if ( isset( $wp_query->query_vars['posts_per_page'] ) && ! empty( $wp_query->query_vars['posts_per_page'] ) ) {
$posts_per_page = $wp_query->query_vars['posts_per_page'];
} else {
// fall back to the site option
$posts_per_page = get_option( 'posts_per_page' );
}
// accommodate the offset if applicable
$offset = 0;
if ( isset( $wp_query->query_vars['offset'] ) && ! empty( $wp_query->query_vars['offset'] ) ) {
$offset = $wp_query->query_vars['offset'];
}
$args = array(
'terms' => $terms,
'page' => $wpPaged,
'order' => $order,
'posts_per_page' => apply_filters( 'searchwp_posts_per_page', intval( $posts_per_page ), 'default', $terms, $wpPaged ),
'offset' => apply_filters( 'searchwp_query_offset', intval( $offset ), 'default', $terms, $wpPaged ),
);
// @since 3.0.6 admin searching requires an engine.
if ( isset( $admin_engine ) && ! empty( $admin_engine ) ) {
$args['engine'] = $admin_engine;
}
// perform the search
$profiler = array( 'before' => microtime() );
$searchwp = $this->do_search( $args );
$profiler['after'] = microtime();
$this->active = false;
$this->isMainQuery = false;
$this->results_weights = $searchwp->results_weights;
// we need to tell WP Query about everything that's different as per these better results
$wp_query->found_posts = absint( $searchwp->foundPosts );
$wp_query->max_num_pages = absint( $searchwp->maxNumPages );
do_action( 'searchwp_log', 'found_posts = ' . $wp_query->found_posts );
do_action( 'searchwp_log', 'max_num_pages = ' . $wp_query->max_num_pages );
// store diagnostics for debugging
$this->diagnostics[] = array(
'engine' => 'default',
'terms' => $args['terms'],
'found_posts' => $searchwp->foundPosts,
'posts' => $searchwp->posts,
'profiler' => $profiler,
'args' => $args,
);
$posts = apply_filters( 'searchwp_results', $searchwp->posts, array(
'terms' => $terms,
'page' => $wpPaged,
'order' => $order,
'foundPosts' => $wp_query->found_posts,
'maxNumPages' => $wp_query->max_num_pages,
'engine' => 'default',
) );
// Allow devs ability to re-count results if they choose to manipulate them.
$wp_query->found_posts = apply_filters( 'searchwp_results_found_posts', $wp_query->found_posts, 'default' );
$wp_query->max_num_pages = apply_filters( 'searchwp_results_max_num_pages', $wp_query->max_num_pages, 'default' );
$wp_query->posts = $posts;
$wp_query->post_count = $wp_query->found_posts;
return $posts;
} elseif ( is_search() ) {
// there were no valid search terms so we need to reset everything and return no results
$this->active = false;
$this->isMainQuery = false;
$wp_query->found_posts = 0;
$wp_query->max_num_pages = 0;
$posts = array();
}
return $posts;
}
private function do_search( $args ) {
// Reset the suggested terms.
$this->suggested_terms = array();
$searchwp = new SearchWPSearch( $args );
$found_posts = isset( $searchwp->foundPosts ) ? absint( $searchwp->foundPosts ) : 0;
// If no results were found, trigger 'did you mean?' functionality.
if (
empty( $found_posts )
&& apply_filters( 'searchwp_do_suggestions', false, array( $args ) )
) {
do_action( 'searchwp_log', 'No results for original query, finding closest match' );
$engine = ! empty( $args['engine'] ) ? $args['engine'] : 'default';
$this->suggested_terms = $this->get_search_suggestion( $args['terms'], $engine );
// If there are suggested terms that differ from the original query...
if (
strtolower( implode( ' ', $this->suggested_terms ) ) !== strtolower( $this->original_query )
&& ! empty( $this->suggested_terms )
) {
// The suggested terms are a key-by-key replacement of suggestions.
$args['original_terms'] = $args['terms'];
$args['terms'] = $this->suggested_terms;
do_action( 'searchwp_log', 'Found search suggestion, re-running search' );
// TODO: To consider: should this search be logged?
$searchwp = new SearchWPSearch( $args );
// Adds a flag for subsequent handling.
$args['type'] = 'did-you-mean';
// Let developers control the output if they want.
do_action( 'searchwp_revised_search_query', $args );
// Re-set the found posts.
$found_posts = isset( $searchwp->foundPosts ) ? absint( $searchwp->foundPosts ) : 0;
// Default behavior is to output a notice before The Loop.
if (
! empty( $found_posts )
&& apply_filters( 'searchwp_auto_output_revised_search_query', true, $args )
) {
if ( ! version_compare( PHP_VERSION, '5.4', '<' ) ) {
// Prevent PHP Fatal by including the closure.
include SWP()->dir . '/includes/closure.output-revised-search.php';
} else {
do_action( 'searchwp_log', '! Search suggestion output disabled: please upgrade to PHP 5.4 or higher' );
}
}
} else {
do_action( 'searchwp_log', 'No search suggestions found' );
}
} else {
do_action( 'searchwp_log', 'Skipping search suggestions' );
}
return $searchwp;
}
public function get_search_suggestion( $terms, $engine = 'default' ) {
$suggested_terms = array();
if ( version_compare( PHP_VERSION, '5.4', '<' ) ) {
do_action( 'searchwp_log', '! Proper search suggestion generation is not possible, please upgrade to PHP 5.4 or higher' );
return $suggested_terms;
}
$engine = $this->is_valid_engine( $engine ) ? $engine : 'default';
if ( ! is_array( $terms ) ) {
$terms = explode( ' ', $terms );
}
include_once SWP()->dir . '/includes/class.partial-matches.php';
$suggestion_engine = new SearchWPPartialMatches();
// We're going to loop through the search terms and find
// the best suggestion for each term that's not in the index
foreach ( (array) $terms as $original_term ) {
$fuzzy_terms = $suggestion_engine->find_fuzzy_matches(
array( $original_term ),
$engine,
$original_term
);
$suggested_term = '';
$shortest = -1;
// Loop through search query and replace anything without a match with the closest match.
if ( ! empty( $fuzzy_terms ) ) {
foreach ( $fuzzy_terms as $fuzzy_term ) {
$lev = levenshtein( $fuzzy_term, $original_term );
if ( 0 === $lev ) {
// Exact match...
$suggested_term = $fuzzy_term;
$shortest = $lev;
break;
}
if ( $lev <= $shortest || $shortest < 0 ) {
$suggested_term = $fuzzy_term;
$shortest = $lev;
}
}
if ( ! empty( $suggested_term ) ) {
$suggested_terms[] = $suggested_term;
}
$suggested_term = '';
$shortest = -1;
}
}
return $suggested_terms;
}
public function output_revised_search( $args ) {
echo '<p class="searchwp-revised-search-notice">';
echo wp_kses(
sprintf(
// Translators: Placeholder is the revised search string.
__( 'Showing results for <em class="searchwp-suggested-revision-query">%s</em>', 'searchwp' ),
esc_html( implode( ' ', $args['terms'] ) )
),
array(
'em' => array(
'class' => array(),
),
)
);
echo '</p>';
}
/**
* Strip whitelist terms from content so as to make them truly exclusive
*
* @since 2.6.2
*
* @param $content
* @param $whitelisted_terms
*
* @return mixed|string
*/
function process_exclusive_regex_matches( $content, $whitelisted_terms ) {
if ( empty( $whitelisted_terms ) ) {
return $content;
}
// add the buffer the entire string so we can whole-word replace
$content = ' ' . $content . ' ';
// also need to buffer the whitelisted terms to prevent replacement overrun
foreach ( (array) $whitelisted_terms as $key => $val ) {
$whitelisted_terms[ $key ] = ' ' . $val . ' ';
}
// remove the matches
$content = str_ireplace( $whitelisted_terms, ' ', $content );
// clean up the double space flag we used
$content = str_replace( ' ', ' ', $content );
$content = trim( $content );
return $content;
}
/**
* Callback for admin_menu action; adds SearchWP link to Settings menu in the WordPress admin
*
* @since 1.0
*/
function admin_menu() {
$options_page = add_options_page( $this->pluginName, __( $this->pluginName, 'searchwp' ), $this->settings_cap, $this->textDomain, array( $this, 'options_page' ) );
add_dashboard_page( __( 'Search Statistics', 'searchwp' ), __( 'Search Stats', 'searchwp' ), apply_filters( 'searchwp_statistics_cap', 'publish_posts' ), $this->textDomain . '-stats', array( $this, 'stats_page' ) );
$show_legacy_ui = apply_filters( 'searchwp_legacy_settings_ui', false );
if ( ! empty( $show_legacy_ui ) ) {
add_action( 'load-' . $options_page, array( $this, 'get_indexer_communication_result' ) );
}
}
/**
* Callback for admin_enqueue_scripts. Enqueues our assets.
*
* @param $hook string
*
* @since 1.0
*/
function assets( $hook ) {
$base_url = trailingslashit( $this->url );
if ( 'dashboard_page_searchwp-stats' == $hook ) {
wp_register_style( 'swp_stats_css', $base_url . 'assets/css/searchwp-stats.css', false, $this->version );
wp_register_style( 'chartist', $base_url . 'assets/vendor/chartist/chartist.min.css', false, '0.1.15' );
wp_register_script( 'chartist', $base_url . 'assets/vendor/chartist/chartist.min.js', array(), '0.1.15' );
wp_enqueue_script( 'chartist' );
wp_enqueue_style( 'chartist' );
wp_enqueue_style( 'swp_stats_css' );
}
if ( 'post.php' == $hook ) {
wp_enqueue_script( 'heartbeat' );
add_action( 'admin_print_footer_scripts', array( $this, 'heartbeat_last_indexed' ), 20 );
}
}
/**
* Utilize the WordPress Heartbeat API to dynamically update the Last Indexed time after updating posts
*/
function heartbeat_last_indexed() {
global $post;
?>
<script>
(function($){
// hook into the heartbeat-send
$(document).on('heartbeat-send', function(e, data) {
data['searchwp_heartbeat_action'] = 'last_indexed';
data['searchwp_heartbeat_object'] = <?php echo isset( $post->ID ) ? absint( $post->ID ) : 0; ?>;
});
// listen for the custom event "heartbeat-tick" on $(document).
$(document).on( 'heartbeat-tick', function(e, data) {
// if our data isn't present, short circuit
if ( ! data['searchwp_last_indexed'] ) {
return;
}
// update the last indexed time
$('#wp-admin-bar-searchwp_last_indexed > div').text( "<?php _e( 'Last indexed', 'searchwp' ); ?> " + data['searchwp_last_indexed'] );
});
}(jQuery));
</script>
<?php
}
/**
* Callback for the WordPress Heartbeat API. Currently used to dynamically update the Last Index time after editing posts.
*
* @param $response
* @param $data
*
* @return mixed
*/
function heartbeat_received( $response, $data ) {
// maybe retrieve our last indexed time
if ( isset( $data['searchwp_heartbeat_action'] ) && 'last_indexed' == $data['searchwp_heartbeat_action'] ) {
$object_id = absint( $data['searchwp_heartbeat_object'] );
// Send back the number of complete payments
$response['searchwp_last_indexed'] = $this->get_last_indexed_time( $object_id, true );
}
return $response;
}
/**
* Outputs the stats page and all stats
*
* @since 1.0
*/
function stats_page() {
$show_legacy = apply_filters( 'searchwp_legacy_stats', false );
if ( empty( $show_legacy ) ) {
include( dirname( __FILE__ ) . '/admin/stats.php' );
} else {
include( dirname( __FILE__ ) . '/admin/stats-legacy.php' );
}
}
/**
* Truncates log table
*
* @since 1.6.5
*/
function reset_stats() {
global $wpdb;
do_action( 'searchwp_log', 'reset_stats()' );
$prefix = $wpdb->prefix . SEARCHWP_DBPREFIX;
// truncate the log table
foreach ( $this->tables as $table ) {
if ( 'log' == $table['table'] ) {
$tableName = $wpdb->prepare( '%s', $prefix . $table['table'] );
if ( "'" == substr( $tableName, 0, 1 ) && "'" == substr( $tableName, strlen( $tableName ) - 1 ) ) {
$tableName = '`' . substr( $tableName, 1, strlen( $tableName ) - 2 ) . '`';
}
$wpdb->query( "TRUNCATE TABLE {$tableName}" );
}
}
$this->reset_dashboard_stats_transients();
// Because we track hashes of ignored queries these need to be reset too.
searchwp_set_setting( 'ignored_queries', array() );
}
/**
* Delete the transient that stores Dashboard stats
*
* @since 2.5.5
*
* @return void
*/
public function reset_dashboard_stats_transients() {
// also remove the transients for Dashboard Widget
foreach ( $this->settings['engines'] as $engine => $engineSettings ) {
$transient_today_key = 'swp_stats_' . md5( 'searchwp_widget_stats_today_' . $engine );
$transient_month_key = 'swp_stats_' . md5( 'searchwp_widget_stats_month_' . $engine );
delete_transient( $transient_today_key );
delete_transient( $transient_month_key );
}
}
/**
* Completely truncates all index tables, removes all index-related options
*
* @since 1.0
*/
function purge_index() {
global $wpdb;
do_action( 'searchwp_log', 'purge_index()' );
$prefix = $wpdb->prefix . SEARCHWP_DBPREFIX;
foreach ( $this->tables as $table ) {
if ( 'log' !== $table['table'] ) {
$tableName = $wpdb->prepare( '%s', $prefix . $table['table'] );
if ( "'" == substr( $tableName, 0, 1 ) && "'" == substr( $tableName, strlen( $tableName ) - 1 ) ) {
$tableName = '`' . substr( $tableName, 1, strlen( $tableName ) - 2 ) . '`';
}
$wpdb->query( "TRUNCATE TABLE {$tableName}" );
}
}
// remove all metadata flags
$wpdb->delete( $wpdb->prefix . 'postmeta', array( 'meta_key' => '_' . SEARCHWP_PREFIX . 'last_index' ) );
$wpdb->delete( $wpdb->prefix . 'postmeta', array( 'meta_key' => '_' . SEARCHWP_PREFIX . 'attempts' ) );
$wpdb->delete( $wpdb->prefix . 'postmeta', array( 'meta_key' => '_' . SEARCHWP_PREFIX . 'skip' ) );
$wpdb->delete( $wpdb->prefix . 'postmeta', array( 'meta_key' => '_' . SEARCHWP_PREFIX . 'skip_doc_processing' ) );
$wpdb->delete( $wpdb->prefix . 'postmeta', array( 'meta_key' => '_' . SEARCHWP_PREFIX . 'review' ) );
// Remove any persisted extra metadata.
$persisted_extra_metadata = searchwp_get_persisted_extra_metadata_keys();
if ( ! empty( $persisted_extra_metadata ) ) {
foreach ( $persisted_extra_metadata as $persisted_extra_metadata_key ) {
$wpdb->delete(
$wpdb->prefix . 'postmeta',
array(
'meta_key' => $persisted_extra_metadata_key
)
);
}
}
// Don't use this filter anymore, use searchwp_purge_document_content
$_legacy_purge_pdf_content = apply_filters( 'searchwp_purge_pdf_content', false );
if ( apply_filters( 'searchwp_purge_document_content', $_legacy_purge_pdf_content, array() ) ) {
$wpdb->delete( $wpdb->prefix . 'postmeta', array( 'meta_key' => SEARCHWP_PREFIX . 'content' ) );
}
$wpdb->delete( $wpdb->prefix . 'postmeta', array( 'meta_key' => SEARCHWP_PREFIX . 'pdf_metadata' ) );
// kill all the options related to the index
searchwp_wake_up_indexer();
searchwp_set_setting( 'initial_index_built', false );
searchwp_set_setting( 'notices', array() );
searchwp_set_setting( 'valid_db_environment', false );
searchwp_delete_option( 'indexnonce' );
searchwp_update_option( 'purge_queue', array() );
delete_option( 'searchwp_transient' );
delete_option( 'swppurge_transient' );
// reset the counts
if ( class_exists( 'SearchWPIndexer' ) ) {
$indexer = new SearchWPIndexer();
$indexer->update_running_counts();
}
}
/**
* Output the markup for posts that failed to make it into the index
*
* @since 1.3
*/
function show_erroneous_posts() {
$args = array(
'posts_per_page' => -1,
'post_type' => 'any',
'post_status' => array( 'publish', 'inherit' ),
'fields' => 'ids',
'suppress_filters' => true,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_' . SEARCHWP_PREFIX . 'last_index',
'value' => '', // http://core.trac.wordpress.org/ticket/23268
'compare' => 'NOT EXISTS',
'type' => 'NUMERIC',
),
array( // only want media that hasn't failed indexing multiple times
'key' => '_' . SEARCHWP_PREFIX . 'skip',
'compare' => 'EXISTS',
'type' => 'BINARY',
)
)
);
if ( ( isset( $_GET['action'] ) && 'reintroduce' == strtolower( $_GET['action'] ) ) && isset( $_GET['swpid'] ) ) {
$erroneous_post_id = absint( $_GET['swpid'] );
if ( isset( $_GET['swperroneous'] ) && wp_verify_nonce( $_GET['swperroneous'], 'swperroneouspost' . $erroneous_post_id ) ) {
// remove the flags preventing the post from being indexed
$this->purge_post( $erroneous_post_id );
$this->trigger_index();
}
} else {
if ( isset( $_GET['action'] ) && 'reintroduce_all' == strtolower( $_GET['action'] ) ) {
if ( isset( $_GET['swperroneouspurge'] ) && wp_verify_nonce( $_GET['swperroneouspurge'], 'swperroneouspurge' ) ) {
// grab all erroneous posts
$erroneous_posts = get_posts( $args );
if ( ! empty( $erroneous_posts ) ) {
foreach ( $erroneous_posts as $erroneous_post_id ) {
$this->purge_post( absint( $erroneous_post_id ) );
}
$this->trigger_index();
}
}
}
}
$erroneousPosts = get_posts( $args );
// WordPress 4.2 introduced a move to utf8mb4, SearchWP 2.5.7 followed suit
// but if the conversion from utf8 to utf8mb4 failed, it was logged
// e.g. if emoji were being used on a site where the upgrade failed, the
// post would fail to index, so we can call that out right here
$utf8mb4_failed_upgrade = false;
if ( searchwp_get_option( 'utf8mb4_upgrade_failed' ) ) {
$utf8mb4_failed_upgrade = true;
}
?>
<style type="text/css">
#searchwp-index-errors-notice { display:none; }
</style>
<div class="wrap">
<div id="icon-searchwp" class="icon32">
<img src="<?php echo esc_url( trailingslashit( $this->url ) ); ?>assets/images/searchwp@2x.png" alt="SearchWP" width="21" height="32" />
</div>
<h2><?php echo esc_html( $this->pluginName ) . ' ' . __( 'Outstanding Index Issues' ); ?></h2>
<?php if ( empty( $erroneousPosts ) ) : ?>
<p><?php _e( 'Nothing is currently excluded from the indexer.', 'searchwp' ); ?></p>
<?php else : ?>
<?php if ( $utf8mb4_failed_upgrade ) : ?>
<div class="error">
<p><?php _e( 'SearchWP was unable to fully implement <code>utf8mb4</code> (Emoji) support which may prevent indexing some content. Please open a support ticket for more assistance.', 'searchwp' ); ?></p>
</div>
<?php endif; ?>
<?php
$nonce_main = wp_create_nonce( 'swperroneous' );
$link_url = admin_url( 'options-general.php?page=searchwp' ) . '&nonce=' . esc_attr( $nonce_main ) . '&action=reintroduce_all&swperroneouspurge=' . esc_attr( wp_create_nonce( 'swperroneouspurge' ) );
?>
<p><?php _e( 'SearchWP was unable to index the following content, and it is actively being excluded from subsequent index runs.', 'searchwp' ); ?> <a href="<?php echo esc_url( $link_url ); ?>" class="button"><?php _e( 'Reintroduce All' ,'searchwp' ); ?></a></p>
<table class="swp-table swp-erroneous-posts">
<colgroup>
<col id="swp-erroneous-posts-titles" />
<col id="swp-erroneous-posts-action" />
</colgroup>
<thead>
<tr>
<th><?php _e( 'Title', 'searchwp' ); ?></th>
<th><?php _e( 'Reintroduce to indexer', 'searchwp' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $erroneousPosts as $erroneousPost ) :
if ( ! isset( $_GET['swpid'] ) || ( isset( $_GET['swpid'] ) && ( absint( $_GET['swpid'] ) != $erroneousPost ) ) ) :
$post_obj = get_post( $erroneousPost ); ?>
<tr>
<td><a href="<?php echo esc_url( admin_url( 'post.php?post=' . absint( $erroneousPost ) . '&action=edit' ) ); ?>">
<?php echo esc_html( get_the_title( $erroneousPost ) ); ?></a>
<?php if ( 'application/pdf' == $post_obj->post_mime_type ) : ?>
<br /><span class="description"><?php _e( 'Manually populate PDF content if reintroduction fails', 'searchwp' ); ?></span>
<?php endif; ?>
</td>
<?php
$link_url = admin_url( 'options-general.php?page=searchwp' ) . '&nonce=' . esc_attr( $nonce_main ) . '&action=reintroduce&swpid=' . absint( $erroneousPost ) . '&swperroneous=' . esc_attr( wp_create_nonce( 'swperroneouspost' . absint( $erroneousPost ) ) );
?>
<td><a href="<?php echo esc_url( $link_url ); ?>"><?php _e( 'Reintroduce', 'searchwp' ); ?></a></td>
</tr>
<?php endif; endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<p>
<a href="<?php echo esc_url( admin_url( 'options-general.php?page=searchwp' ) ); ?>"><?php _e( 'Back to SearchWP Settings', 'searchwp' ); ?></a>
</p>
</div>
<?php
}
/**
* Output the markup for the advanced settings page
*
* @since 1.0
* @deprecated 2.6
*/
function advanced_settings() {}
/**
* Force an indexer hash match and trigger an index update
*
* @since 2.5
*
* @return string Indexer hash
*/
private function trigger_forced_indexer_chunk() {
$hash = sprintf( '%.22F', microtime( true ) ); // inspired by $doing_wp_cron
update_option( 'searchwp_transient', $hash, 'no' );
// trigger an index of this chunk
$indexer = new SearchWPIndexer( $hash );
$indexer->update_running_counts();
return $indexer->hash;
}
/**
* AJAX callback for alternate indexer request
*
* @since 2.5
*/
function handle_alternate_indexer_request() {
check_ajax_referer( 'searchwp_alternate_indexer', 'nonce' );
if ( ! current_user_can( $this->settings_cap ) ) {
wp_die( esc_html__( 'Invalid request', 'searchwp' ) );
}
// If the index is dirty, we need to purge it first
$index_dirty = searchwp_get_setting( 'index_dirty' );
if ( ! empty( $index_dirty ) ) {
searchwp_set_setting( 'index_dirty', false );
$this->purge_index();
}
$next_hash = $this->trigger_forced_indexer_chunk();
wp_cache_flush();
die( json_encode( array( 'hash' => $next_hash ) ) );
}
/**
* Output the HTML for the alternate indexer
*
* @since 2.5
*/
function alternate_indexer_view() {
if ( ! current_user_can( $this->settings_cap ) ) {
wp_die( __( 'Invalid request', 'searchwp' ) );
}
$progress = searchwp_get_option( 'progress' );
$form_action_url_params = array(
'page' => $this->textDomain,
);
$form_action_url = add_query_arg( $form_action_url_params, admin_url( 'options-general.php' ) );
?>
<?php if ( isset( $_REQUEST['swpnonce'] ) ) : ?>
<div class="wrap">
<h3><?php _e( 'SearchWP Alternate Indexer', 'searchwp' ); ?></h3>
<?php $nonce = wp_create_nonce( 'searchwp_alternate_indexer' ); ?>
<p><?php _e( 'Current progress:', 'searchwp' ); ?> <?php echo esc_html( $progress ); ?>%</p>
<p class="descripttion"><?php _e( 'If the percentage is not increasing it <strong>does not necessarily mean</strong> there is a problem. SearchWP takes multiple passes when indexing, please allow adequate time for the indexer to run.', 'searchwp' ); ?></p>
<?php if ( absint( $progress ) < 100 ) : ?>
<p>
<?php _e( 'Triggering next index chunk, please wait...', 'searchwp' ); ?>
<span class="spinner is-active" style="display:inline-block;float:none;position:relative;top:-4px;"></span>
</p>
<script type="text/javascript">
jQuery(document).ready(function($){
// noinspection JSUnresolvedVariable ajaxurl
$.post(ajaxurl, {
action: 'searchwp_alternate_indexer_trigger',
nonce: '<?php echo esc_js( $nonce ); ?>',
swpnonce: '<?php echo esc_js( $_REQUEST['swpnonce'] ); ?>'
},
function (res) {
if (res.hash) {
document.location.href = '<?php echo esc_url( $form_action_url ); ?>&swpnonce=' + res.hash;
} else {
document.location.href = '<?php echo esc_url( $form_action_url ); ?>';
}
}, 'json');
});
</script>
<?php else : ?>
<p><strong><?php _e( 'Index built!', 'searchwp' ); ?></strong></p>
<script type="text/javascript">
document.location.href = '<?php echo esc_url( $form_action_url ); ?>';
</script>
<?php endif; ?>
</div>
<?php else : ?>
<?php
$indexer = new SearchWPIndexer();
$total = intval( $indexer->count_total_posts() );
$indexed = intval( $indexer->indexed_count() );
if ( $total > $indexed ) {
$num_remaining_posts_to_index = absint( $total - $indexed );
} else {
$num_remaining_posts_to_index = 0;
}
// if there are no more posts to index, we don't need to output any UI to trigger the index
if ( empty( $num_remaining_posts_to_index ) ) {
return;
}
$hash = sprintf( '%.22F', microtime( true ) ); // inspired by $doing_wp_cron
update_option( 'searchwp_transient', $hash, 'no' );
?>
<h3><?php _e( 'SearchWP Alternate Indexer', 'searchwp' ); ?></h3>
<p><?php echo sprintf( __( 'There are <strong>%d</strong> entries left to index. <strong>YOU MUST LEAVE THIS BROWSER WINDOW OPEN</strong> during indexing.', 'searchwp' ), absint( $num_remaining_posts_to_index ) ); ?></p>
<p class="description"><?php _e( 'This action builds the initial index and only needs to be run once.', 'searchwp' ); ?></p>
<form method="get" action="<?php echo esc_url( $form_action_url ); ?>">
<input type="hidden" name="page" value="searchwp" />
<input type="hidden" name="swpnonce" value="<?php echo esc_attr( $hash ); ?>" />
<p style="padding-bottom:2em;">
<button class="button button-primary" type="submit" name="submit"><?php esc_attr_e( 'Build Initial Index', 'searchwp' ); ?></button>
</p>
</form>
<?php endif; ?>
<?php
}
/**
* @since 2.6
* @return bool
*/
function is_using_alternate_indexer() {
return ! empty( $this->alternate_indexer );
}
/**
* This functionality needs to be better organized. It is a conglomeration of functionality
* that was shoehorned into the settings page display and it can be much better utilized
* if broken apart and implemented properly
*
* TODO: organize this
*/
function misc_options_page_pre_hooks() {
// check to see if we're using the alternate indexer
if ( $this->alternate_indexer ) {
// hook in the alternate indexer UI
add_action( 'searchwp_settings_before\default', array( $this, 'alternate_indexer_view' ) );
// $this->alternate_indexer_view();
// is the alternate indexer is in progress?
if ( isset( $_REQUEST['swpnonce'] ) ) {
// the settings UI action will never fire because we're going to essentially short-circuit
// once we've displayed the alternate indexer progress (which we'll do manually now)
$this->alternate_indexer_view();
// prevent any more UI from showing up
return true;
}
}
// check to see if we should show posts that failed indexing
if ( isset( $_REQUEST['nonce'] ) && wp_verify_nonce( $_REQUEST['nonce'], 'swperroneous' ) && current_user_can( $this->settings_cap ) ) {
$this->show_erroneous_posts();
return true;
}
return false;
}
/**
* Callback for our implementation of add_options_page. Displays our options screen.
*
* @uses wpdb
* @uses get_option to get saved SearchWP settings
* @since 1.0
*/
function options_page() {
if ( ! current_user_can( apply_filters( 'searchwp_settings_cap', 'manage_options' ) ) ) {
wp_die( __( 'Invalid request', 'searchwp' ) );
}
if ( $this->misc_options_page_pre_hooks() ) {
return;
}
$this->settings_utils->render_header();
echo '<div class="wrap"><div class="swp-notices swp-group"></div>';
$this->settings_utils->render_view();
$this->settings_utils->render_footer();
echo '</div>';
// if we're on the main settings page (and not using the alternate indexer) trigger the indexer
if (
! $this->alternate_indexer
&& ! $this->indexing
&& isset( $_GET['page'] )
&& 'searchwp' == $_GET['page']
&& ! isset( $_GET['tab'] )
&& false == searchwp_get_setting( 'running' ) ) {
$this->indexing = true;
$this->trigger_index();
}
do_action( 'searchwp_log', 'Shutting down after displaying settings screen' );
$this->shutdown();
}
/**
* Export engine configurations as JSON
*
* @since 2.4.5
*
* @param $engines string|array Engine(s) to export
*
* @param bool $encode
*
* @return mixed|string|void JSON-encoded settings
*/
function export_settings( $engines = null, $encode = true ) {
// default is all engines
$settings = $this->settings['engines'];
// single engine
if ( is_string( $engines ) && array_key_exists( $engines, $this->settings['engines'] ) ) {
$settings = $this->settings['engines'][ $engines ];
}
// array of engines
if ( is_array( $engines ) ) {
$settings = array();
foreach ( $engines as $engine ) {
if ( is_string( $engine ) && array_key_exists( $engine, $this->settings['engines'] ) ) {
$settings[ $engine ] = $this->settings['engines'][ $engine ];
}
}
}
return ! empty( $encode ) ? json_encode( $settings ) : $settings;
}
/**
* Programmatically set engine configurations
*
* @since 2.4.5
*
* @param $settings_json string JSON-encoded string of engine settings
*/
function import_settings( $settings_json ) {
// back up existing settings before import
$do_backup = apply_filters( 'searchwp_do_settings_backup', true );
if ( $do_backup ) {
$settings_backups = searchwp_get_option( 'settings_backup' );
$settings_backups[ current_time( 'timestamp' ) ] = $this->settings;
searchwp_update_option( 'settings_backup', $settings_backups );
}
// parse the import
$settings_to_import = json_decode( (string) $settings_json );
if ( false === $settings_to_import ) {
wp_die( __( 'Invalid settings.', 'searchwp' ) );
}
$settings_to_import = $this->object_to_array( $settings_to_import );
$settings_to_import = $this->validate_settings( array( 'engines' => $settings_to_import ) );
$settings_to_import = $settings_to_import['engines'];
foreach ( $this->settings['engines'] as $engine_key => $engine_config ) {
if ( array_key_exists( $engine_key, $settings_to_import ) ) {
// overwrite engine config
$this->settings['engines'][ $engine_key ] = $settings_to_import[ $engine_key ];
unset( $settings_to_import[ $engine_key ] );
}
}
// if there are any imported engines left over, append them
if ( count( $settings_to_import ) ) {
$this->settings['engines'] = array_merge( $this->settings['engines'], $settings_to_import );
}
// persist the settings
update_option( 'searchwp_settings', $this->settings );
}
/**
* Convert an object into an associative array
*
* @since 2.4.5
*
* @param $d
*
* @return array
*/
private function object_to_array( $d ) {
if ( is_object( $d ) ) {
$d = get_object_vars( $d );
}
if ( is_array( $d ) ) {
return array_map( array( $this, 'object_to_array' ), $d );
} else {
return $d;
}
}
/**
* Retrieve Custom Field keys
* @since 2.3.1
*/
function define_keys() {
global $wpdb;
// retrieve custom field keys to include in the Custom Fields weight table select
/** @noinspection SqlDialectInspection */
$this->keys = $wpdb->get_col( $wpdb->prepare( "
SELECT meta_key
FROM $wpdb->postmeta
WHERE meta_key != %s
AND meta_key != %s
AND meta_key != %s
AND meta_key != %s
AND meta_key NOT LIKE %s
GROUP BY meta_key
",
'_' . SEARCHWP_PREFIX . 'indexed',
'_' . SEARCHWP_PREFIX . 'content',
'_' . SEARCHWP_PREFIX . 'needs_remote',
'_' . SEARCHWP_PREFIX . 'skip',
'_oembed_%'
) );
// allow devs to filter this list
$this->keys = array_unique( apply_filters( 'searchwp_custom_field_keys', $this->keys ) );
// sort the keys alphabetically
if ( $this->keys ) {
natcasesort( $this->keys );
} else {
$this->keys = array();
}
}
/**
* Register our settings with WordPress
*
* @uses add_settings_section as per the WordPress Settings API
* @uses add_settings_field as per the WordPress Settings API
* @uses register_setting as per the WordPress Settings API
* @since 1.0
*/
function init_settings() {
add_settings_section(
SEARCHWP_PREFIX . 'settings',
'SearchWP Settings',
array( $this, 'settings_callback' ),
$this->textDomain
);
add_settings_field(
SEARCHWP_PREFIX . 'settings_field',
'Settings',
array( $this, 'settings_field_callback' ),
$this->textDomain,
SEARCHWP_PREFIX . 'settings'
);
register_setting(
SEARCHWP_PREFIX . 'settings',
SEARCHWP_PREFIX . 'settings',
array( $this, 'validate_settings' )
);
}
/**
* Set up WP cron job for maintenance actions
*
* @since 1.0
*/
function schedule_maintenance() {
if ( ! wp_next_scheduled( 'swp_maintenance' ) ) {
wp_schedule_event( time(), 'daily', 'swp_maintenance' );
}
if ( ! wp_next_scheduled( 'swp_indexer' ) && ! searchwp_get_setting( 'initial_index_built' ) ) {
wp_schedule_event( time(), 'swp_frequent', 'swp_indexer' );
}
}
/**
* Too keep an eye on the initial index process, we're going to set up a five minute
* interval in WP cron
*
* @param $schedules
*
* @return mixed
* @since 1.0
*/
function add_custom_cron_interval( $schedules ) {
// only add this interval if the initial index has not been completed
if ( ! isset( $schedules['swp_frequent'] ) && ! searchwp_get_setting( 'initial_index_built' ) ) {
$schedules['swp_frequent'] = array(
'interval' => 60 * 30,
'display' => __( 'SearchWP Frequent (Every five minutes until initial index is built)' )
);
}
return $schedules;
}
/**
* Callback to WordPress' hourly cron job
*
* @since 1.0
*/
function do_cron() {
// if the initial index hasn't been completed, we're going to ping the indexer
if ( ! searchwp_get_setting( 'initial_index_built' ) ) {
// fire off a request to the index process
do_action( 'searchwp_log', 'Request index (cron)' );
$this->trigger_index();
}
}
/**
* Callback from our call to register_setting() in $this->init_settings
*
* @param $input array The submitted $_POST data
*
* @return mixed array Validated array of settings
* @since 1.0
*/
function validate_settings( $input ) {
$validSettings = $this->settings;
$validCategories = array( 'engines' );
// make sure the input is an array
if ( is_array( $input ) ) {
// sift through our settings category looking for engine config
foreach ( $input as $category => $categorySettings ) {
if ( 'engines' == $category ) {
// make sure the array key is sanitized
$sanitizedCategory = sanitize_key( $category );
$validSettings[ $sanitizedCategory ] = array();
// only proceed if we have a valid settings category
if ( in_array( $sanitizedCategory, $validCategories ) ) {
// we're going to first handle any core settings
switch ( $sanitizedCategory ) {
case 'engines':
foreach ( $categorySettings as $engineName => $engineSettings ) {
$sanitizedEngineName = empty( $engineSettings['searchwp_engine_label'] ) ? sanitize_key( $engineName ) : str_replace( '-', '_', sanitize_title( $engineSettings['searchwp_engine_label'] ) );
while ( isset( $validSettings[ $sanitizedCategory ][ $sanitizedEngineName ] ) ) {
$sanitizedEngineName .= '_copy';
}
$validSettings[ $sanitizedCategory ][ $sanitizedEngineName ] = $this->sanitize_engine_settings( $engineSettings );
if ( ! empty( $engineSettings['searchwp_engine_label'] ) ) {
$validSettings[ $sanitizedCategory ][ $sanitizedEngineName ]['searchwp_engine_label'] = sanitize_text_field( $engineSettings['searchwp_engine_label'] );
}
}
break;
}
}
}
}
}
return $validSettings;
}
/**
* Make sure the submitted engine settings match expectations
*
* @param array $engineSettings
*
* @return array
* @since 1.0
*/
function sanitize_engine_settings( $engineSettings = array() ) {
$validEngineSettings = array();
if ( is_array( $engineSettings ) ) {
foreach ( $engineSettings as $postType => $postTypeSettings ) {
if ( in_array( $postType, $this->postTypes ) ) {
$validEngineSettings[ $postType ] = array();
// store a proper 'enabled' setting
$validEngineSettings[ $postType ]['enabled'] = isset( $postTypeSettings['enabled'] ) && $postTypeSettings['enabled'] ? true : false;
// store proper weights
if ( isset( $postTypeSettings['weights'] ) && is_array( $postTypeSettings['weights'] ) ) {
$validEngineSettings[ $postType ]['weights'] = array();
foreach ( $postTypeSettings['weights'] as $postTypeWeightKey => $weight ) {
if ( in_array( $postTypeWeightKey, $this->validTypes ) ) {
if ( ! is_array( $weight ) ) {
$weight = strpos( (string) $weight, '.' ) ? floatval( $weight ) : intval( $weight );
if ( $weight < -1 ) {
$weight = -1;
}
$validEngineSettings[ $postType ]['weights'][ $postTypeWeightKey ] = $weight;
}
else {
// it's either a taxonomy or custom field, comprised of multiple weights
$validEngineSettings[ $postType ]['weights'][ $postTypeWeightKey ] = array();
foreach ( $weight as $contentName => $subweight ) { // could just check to see if $contentName is 'tax' or 'cf'...
if ( ! is_array( $subweight ) ) {
// taxonomy
$weightKey = sanitize_text_field( $contentName );
$subweight = strpos( (string) $subweight, '.' ) ? floatval( $subweight ) : intval( $subweight );
if ( $subweight < -1 ) {
$subweight = -1;
}
$validEngineSettings[ $postType ]['weights'][ $postTypeWeightKey ][ $weightKey ] = $subweight;
} else {
// custom field
$customFieldFlag = sanitize_text_field( $contentName );
$weight = strpos( (string) $subweight['weight'], '.' ) ? floatval( $subweight['weight'] ) : intval( $subweight['weight'] );
if ( $weight < -1 ) {
$weight = -1;
}
if ( isset( $subweight['metakey'] ) && isset( $subweight['weight'] ) ) {
$validEngineSettings[ $postType ]['weights'][ $postTypeWeightKey ][ $customFieldFlag ] = array(
'metakey' => sanitize_text_field( $subweight['metakey'] ),
'weight' => $weight,
);
}
}
}
}
}
}
}
// dynamically add our taxonomies to valid options array
$taxonomies = get_object_taxonomies( $postType );
if ( is_array( $taxonomies ) && count( $taxonomies ) ) {
foreach ( $taxonomies as $taxonomy ) {
$taxonomy = get_taxonomy( $taxonomy );
$this->validOptions[] = 'exclude_' . $taxonomy->name;
$this->validOptions[] = 'limit_to_' . $taxonomy->name;
}
}
// store proper options
if ( isset( $postTypeSettings['options'] ) && is_array( $postTypeSettings['options'] ) ) {
foreach ( $postTypeSettings['options'] as $engineOptionName => $engineOptionValue ) {
if ( in_array( $engineOptionName, $this->validOptions ) ) {
switch ( $engineOptionName ) {
case 'exclude':
case 'limit_to':
case 'mimes':
if ( 'mimes' === $engineOptionName && is_string( $engineOptionValue ) && '0' === trim( $engineOptionValue ) ) {
// The "All Documents" mime grouping by itself will be a value of zero, which is normally
// unwanted but in this case it's very important
$engineOptionValue = '0';
} else {
// we want a comma separated string of integers
$engineOptionValue = $this->get_integer_csv_string_from_string_or_array( $engineOptionValue );
}
break;
case 'attribute_to':
// this can only be a post ID
$engineOptionValue = absint( $engineOptionValue );
break;
case 'stem':
// this is a bool (either 1 or 0)
$engineOptionValue = absint( $engineOptionValue );
break;
case 'parent':
// this is a bool (either 1 or 0)
$engineOptionValue = absint( $engineOptionValue );
break;
default:
// it's a taxonomy exclusion or limiter
if (
'exclude_' == substr( $engineOptionName, 0, 8 )
|| 'limit_to_' == substr( $engineOptionName, 0, 9 )
) {
$engineOptionValue = $this->get_integer_csv_string_from_string_or_array( $engineOptionValue );
} else {
$engineOptionValue = 0;
}
break;
}
// setting value has been enforced as a string
// which may consist of a bool (1 or 0) or a comma separated list of integers
// but everything is based on that assumption of it being a string in all cases
$validEngineSettings[ $postType ]['options'][ $engineOptionName ] = sanitize_text_field( $engineOptionValue );
}
}
}
}
}
}
return $validEngineSettings;
}
/**
* Generate a string of comma separated integers from an existing string of
* comma separated integers or an array of integers
*
* @since 2.5.6
*
* @param string|array $source Array of integers or string of (maybe comma separated) integers
*
* @return string Comma separated string of integers
*/
function get_integer_csv_string_from_string_or_array( $source = '' ) {
if ( ! is_string( $source ) && ! is_array( $source ) || empty( $source ) ) {
return '';
}
// always want a string
if ( is_array( $source ) ) {
$source = implode( ', ', $source );
}
// check to see whether the string is already comma separated
if ( false !== strpos( $source, ',' ) ) {
$source = explode( ',' , $source );
$source = array_map( 'trim', $source );
$source = array_map( 'absint', $source );
$source = array_unique( $source );
$source = implode( ', ', $source );
} else {
$source = (string) absint( $source );
}
return (string) $source;
}
/**
* Callback from our call to add_settings_section() in $this->init_settings
*
* @since 1.0
*/
function settings_callback() {}
/**
* Callback from our call to add_settings_field() in $this->init_settings. Outputs our (hidden) input field to
* accommodate the Settings API
*
* @since 1.0
*/
function settings_field_callback() {
?><!--suppress HtmlFormInputWithoutLabel -->
<input type="text" name="<?php echo esc_attr( SEARCHWP_PREFIX ); ?>settings" id="<?php echo esc_attr( SEARCHWP_PREFIX ); ?>settings" value="SearchWP" /><?php
}
/**
* Determines whether a post status is applicable
*
* @param int $post_id The post ID to check
*
* @since 2.9.17
*/
function is_applicable_post_status( $post_id ) {
// We need to loop through all engines and retrieve all potential post statuses because this
// post could be a result in any engine
$post_status_whitelist = array();
foreach ( $this->settings['engines'] as $engine => $post_types ) {
$post_status_whitelist = array_merge(
$post_status_whitelist,
(array) apply_filters( 'searchwp_post_statuses', array( 'publish' ), $engine )
);
}
$post_status_whitelist = array_unique( $post_status_whitelist );
$post_status = get_post_status( $post_id );
return in_array( $post_status, $post_status_whitelist );
}
/**
* Purge a post from the index when it is edited
*
* @param $post_id int The edited post
*/
function purge_post_via_edit( $post_id ) {
// make sure we want to actually purge it
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if (
defined( 'DOING_AJAX' )
&& DOING_AJAX
&& ! (
// Quick Edit.
isset( $_REQUEST['action'] )
&& 'inline-save' === $_REQUEST['action']
)
) {
return;
}
if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
return;
}
if ( wp_is_post_revision( $post_id ) ) {
return;
}
// Not all posts should be sent through the purge queue. To keep resource usage as low as possible
// we'll mimic what the indexer does to check for unindexed posts and find out whether
// we need to do anything here or can simply bug out early.
$this_post_type = get_post_type( $post_id );
$indexed_post_types = $this->get_indexed_post_types();
$excluded_ids = apply_filters( 'searchwp_exclude', array(), '', array() );
$excluded_ids = array_map( 'absint', $excluded_ids );
if (
! in_array( $this_post_type, $indexed_post_types, true ) // Excluded post type
|| in_array( $post_id, $excluded_ids ) // Excluded from search results
) {
// There's a (slight) chance this post is in the index already even though it no longer should be.
// It may be because a hook was added along the line after content was already indexed, so we need
// to determine whether there is content in the index for this post type because if there is
// we should utilize the purge queue to get it out so as to reduce the overall index size.
if ( ! $this->is_post_present_in_index( $post_id ) ) {
do_action( 'searchwp_log', 'purge_post_via_edit() SKIPPING ' . $post_id );
return;
} else {
do_action( 'searchwp_log', 'purge_post_via_edit()' . $post_id . ' will not be indexed but needs to be purged' );
$this->purge_post( $post_id, true );
}
}
do_action( 'searchwp_log', 'purge_post_via_edit() ' . $post_id );
if ( ! isset( $this->purgeQueue[ $post_id ] ) ) {
// Implement a threshold to prevent redundant purge
$last_indexed_time = $this->get_last_indexed_time( $post_id, false );
$last_indexed_seconds_ago = current_time( 'timestamp' ) - $last_indexed_time;
$last_indexed_threshold = apply_filters( 'searchwp_last_indexed_threshold', 4 );
if ( absint( $last_indexed_seconds_ago ) > absint( $last_indexed_threshold ) ) {
$this->purgeQueue[ $post_id ] = $post_id;
do_action( 'searchwp_log', 'purge_post_via_edit() ' . $post_id );
} else {
do_action( 'searchwp_log', 'purge_post_via_edit() cancelled, last edit threshold breached for ' . $post_id );
}
} else {
do_action( 'searchwp_log', 'Prevented duplicate purge purge_post_via_edit() ' . $post_id );
}
}
/**
* Determines whether a given post ID is present in the index regardless of whether it's considered fully indexed.
*
* @param $post_id
*
* @return bool Whether the post is in the index.
*/
public function is_post_present_in_index( $post_id = 0 ) {
global $wpdb;
$post_id = intval( $post_id );
if ( $post_id < 1 ) {
return false;
}
$tables = array(
$wpdb->prefix . SEARCHWP_DBPREFIX . 'index',
$wpdb->prefix . SEARCHWP_DBPREFIX . 'tax',
$wpdb->prefix . SEARCHWP_DBPREFIX . 'cf',
);
$present = false;
foreach ( $tables as $table ) {
$present = $wpdb->get_row( $wpdb->prepare( "SELECT post_id FROM {$table} WHERE post_id = %d LIMIT 1", $post_id ) );
if ( $present ) {
break;
}
}
return $present;
}
/**
*
* !!!!!!!!!!!!!!!!!! NOT USABLE AT THIS TIME
* Purge a post from the index when its metadata is edited
*
* @param $meta_id
* @param $object_id
* @param $meta_key
* @param $_meta_value
*
* @return void
*/
private function _purge_post_via_update_post_metadata( $meta_id, $object_id, $meta_key, $_meta_value ) {
if ( true ) {
return;
}
// There are certain meta keys we don't want to consider, else we'd be constantly purging posts
if ( in_array( $meta_key, array(
'_edit_lock',
'_edit_last',
'_wp_old_slug',
) ) ) {
return;
}
// Prevent redundancy; this hook is fired for each meta record for a post
remove_filter( 'update_post_metadata', array( $this, 'purge_post_via_update_post_metadata' ), 999, 5 );
// Extracted document content is a special case
if ( apply_filters( 'searchwp_purge_document_content', false, array( 'post_id' => $object_id ) ) ) {
delete_post_meta( $object_id, SEARCHWP_PREFIX . 'content' );
delete_post_meta( $object_id, SEARCHWP_PREFIX . 'pdf_metadata' );
}
// We need to manually force the purge of this post because many times shutdown is not fired
if ( ! isset( $this->purgeQueue[ $object_id ] ) ) {
$this->purgeQueue[ $object_id ] = $object_id;
do_action( 'searchwp_log', 'purge_post_via_edit_meta() ' . $object_id );
$this->setup_purge_queue();
}
return;
}
/**
* Removes all record of a post and it's content from the index and triggers a reindex
*
* @param $post_id
*
* @return bool
*/
function purge_post( $post_id, $skip_purge_queue = false ) {
global $wpdb;
$post_id = absint( $post_id );
do_action( 'searchwp_log', 'purge_post() ' . $post_id );
if ( empty( $skip_purge_queue ) ) {
$this->purgeQueue[ $post_id ] = $post_id;
}
// remote it from the index
$wpdb->delete( $wpdb->prefix . SEARCHWP_DBPREFIX . 'index', array( 'post_id' => $post_id ), array( '%d' ) );
$wpdb->delete( $wpdb->prefix . SEARCHWP_DBPREFIX . 'tax', array( 'post_id' => $post_id ), array( '%d' ) );
$wpdb->delete( $wpdb->prefix . SEARCHWP_DBPREFIX . 'cf', array( 'post_id' => $post_id ), array( '%d' ) );
// remove the postmeta
delete_post_meta( $post_id, '_' . SEARCHWP_PREFIX . 'last_index' );
delete_post_meta( $post_id, '_' . SEARCHWP_PREFIX . 'attempts' );
delete_post_meta( $post_id, '_' . SEARCHWP_PREFIX . 'skip' );
delete_post_meta( $post_id, '_' . SEARCHWP_PREFIX . 'review' );
delete_post_meta( $post_id, '_' . SEARCHWP_PREFIX . 'terms' );
if ( apply_filters( 'searchwp_purge_document_content', false, array( 'post_id' => $post_id ) ) ) {
delete_post_meta( $post_id, '_' . SEARCHWP_PREFIX . 'content' );
delete_post_meta( $post_id, '_' . SEARCHWP_PREFIX . 'pdf_metadata' );
}
return true;
}
/**
* Callback for actions related to comments changing
*
* @uses $this->purge_post to clear out the post content from the index and trigger a reindex entirely
*
* @param $id
*/
function purge_post_via_comment( $id ) {
if ( apply_filters( 'searchwp_index_comments', true ) ) {
$comment = get_comment( $id );
$object_id = absint( $comment->comment_post_ID );
if ( ! isset( $this->purgeQueue[ $object_id ] ) ) {
$this->purgeQueue[ $object_id ] = $object_id;
do_action( 'searchwp_log', 'purge_post_via_comment() ' . $object_id );
} else {
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', 'Prevented duplicate purge purge_post_via_comment() ' . $object_id );
}
}
}
}
/**
* Callback for a new comment being posted
*
* @uses $this->purge_post to clear out the post content from the index and trigger a reindex entirely
*
* @param $id
*/
function purge_post_via_comment_post( $id, $comment_approved ) {
if ( 1 === $comment_approved && apply_filters( 'searchwp_index_comments', true ) ) {
$comment = get_comment( $id );
$object_id = absint( $comment->comment_post_ID );
if ( ! isset( $this->purgeQueue[ $object_id ] ) ) {
$this->purgeQueue[ $object_id ] = $object_id;
do_action( 'searchwp_log', 'purge_post_via_comment_post() ' . $object_id );
} else {
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', 'Prevented duplicate purge purge_post_via_comment_post() ' . $object_id );
}
}
}
}
/**
* Callback for a comment status transition happening
*
* @uses $this->purge_post to clear out the post content from the index and trigger a reindex entirely
*
* @param $id
*/
function purge_post_via_comment_transition( $new_status, $old_status, $comment ) {
if ( $new_status === $old_status ) {
return;
}
// If a comment status changes, the index needs to be updated
if ( apply_filters( 'searchwp_index_comments', true ) ) {
$object_id = absint( $comment->comment_post_ID );
if ( ! isset( $this->purgeQueue[ $object_id ] ) ) {
$this->purgeQueue[ $object_id ] = $object_id;
do_action( 'searchwp_log', 'purge_post_via_comment_transition() ' . $object_id );
} else {
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', 'Prevented duplicate purge purge_post_via_comment_transition() ' . $object_id );
}
}
}
}
/**
* Add a post to a purge queue after any of it's terms were changed
*
* @param $object_id
* @param $terms
* @param $tt_ids
* @param $taxonomy
* @param $append
* @param $old_tt_ids
*/
function purge_post_via_term( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( false !== wp_is_post_revision( $object_id ) ) {
return;
}
$object_id = absint( $object_id );
// prevent repeated purging of the same post
if ( ! isset( $this->purgeQueue[ $object_id ] ) ) {
// Only mark for purge if the taxonomy applies
if ( $this->is_used_taxonomy( $taxonomy ) ) {
$this->purgeQueue[ $object_id ] = $object_id;
do_action( 'searchwp_log', 'purge_post_via_term() ' . $object_id );
} else {
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', 'purge_post_via_term() skipped, taxonomy not used: ' . $taxonomy );
}
}
}
else {
$detailed_debug = apply_filters( 'searchwp_debug_detailed', false );
if ( $detailed_debug ) {
do_action( 'searchwp_log', 'Prevented duplicate purge purge_post_via_term() ' . $object_id );
}
}
}
/**
* Trigger a reindex
*/
/** @noinspection PhpUnusedPrivateMethodInspection */
private function trigger_reindex() {
// check capabilities
if (
! current_user_can( 'edit_posts' ) &&
! current_user_can( 'edit_pages' ) &&
! current_user_can( $this->settings_cap )
) {
do_action( 'searchwp_log', 'Failed capabilities check in triggerReindex()' );
return false;
}
do_action( 'searchwp_log', 'Request index (reindex)' );
$this->trigger_index();
return true;
}
/**
* Callback for plugin activation, outputs admin notice
*
* @since 1.0
*/
function activation() {
if ( false == searchwp_get_setting( 'activated' ) ) {
searchwp_set_setting( 'activated', true );
// Initialize indexer counts
if ( class_exists( 'SearchWPIndexer' ) ) {
$indexer = new SearchWPIndexer();
$indexer->update_running_counts();
}
// Redirect to settings screen
$settings_screen_url = add_query_arg(
array(
'page' => 'searchwp',
'welcome' => '1',
),
esc_url( admin_url( 'options-general.php' ) )
);
wp_safe_redirect( $settings_screen_url );
}
}
/**
* Register meta box for document content textarea
*
* @since 1.0
*/
function document_content_meta_box() {
add_meta_box(
'searchwp_doc_content',
__( 'SearchWP File Content', 'searchwp' ),
array( $this, 'document_content_meta_box_markup' ),
'attachment'
);
}
function get_mimes_from_settings_ids( $mimes ) {
// We need an array of integers that match the 'reference' IDs used in the settings that correlate to mime type groups
if ( ! is_array( $mimes ) ) {
if ( false !== strpos( $mimes, ',' ) ) {
$mimes = explode( ',', $mimes );
} else {
$mimes = array( $mimes );
}
}
$mimes = array_map( 'absint', $mimes );
$targetedMimes = array();
// TODO: Better system for this
$mimeref = array(
'image' => array(
'image/jpeg',
'image/gif',
'image/png',
'image/bmp',
'image/tiff',
'image/x-icon',
),
'video' => array(
'video/x-ms-asf',
'video/x-ms-wmv',
'video/x-ms-wmx',
'video/x-ms-wm',
'video/avi',
'video/divx',
'video/x-flv',
'video/quicktime',
'video/mpeg',
'video/mp4',
'video/ogg',
'video/webm',
'video/x-matroska',
),
'text' => array(
'text/plain',
'text/csv',
'text/tab-separated-values',
'text/calendar',
'text/richtext',
'text/css',
'text/html',
),
'audio' => array(
'audio/mpeg',
'audio/x-realaudio',
'audio/wav',
'audio/ogg',
'audio/midi',
'audio/x-ms-wma',
'audio/x-ms-wax',
'audio/x-matroska',
),
'application' => array(
'application/rtf',
'application/javascript',
'application/pdf',
'application/x-shockwave-flash',
'application/java',
'application/x-tar',
'application/zip',
'application/x-gzip',
'application/rar',
'application/x-7z-compressed',
'application/x-msdownload',
),
'msoffice' => array(
'application/msword',
'application/vnd.ms-powerpoint',
'application/vnd.ms-write',
'application/vnd.ms-excel',
'application/vnd.ms-access',
'application/vnd.ms-project',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-word.document.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'application/vnd.ms-word.template.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel.sheet.macroEnabled.12',
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'application/vnd.ms-excel.template.macroEnabled.12',
'application/vnd.ms-excel.addin.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.template',
'application/vnd.ms-powerpoint.template.macroEnabled.12',
'application/vnd.ms-powerpoint.addin.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.slide',
'application/vnd.ms-powerpoint.slide.macroEnabled.12',
'application/onenote',
),
'openoffice' => array(
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.graphics',
'application/vnd.oasis.opendocument.chart',
'application/vnd.oasis.opendocument.database',
'application/vnd.oasis.opendocument.formula',
),
'wordperfect' => array(
'application/wordperfect',
),
'iwork' => array(
'application/vnd.apple.keynote',
'application/vnd.apple.numbers',
'application/vnd.apple.pages',
),
);
foreach ( $mimes as $mimeKey ) {
switch ( intval( $mimeKey ) ) {
case 1: // PDFs
$targetedMimes = array_merge( $targetedMimes, array( 'application/pdf' ) );
break;
case 2: // Plain Text
$targetedMimes = array_merge( $targetedMimes, $mimeref['text'] );
break;
case 3: // Images
$targetedMimes = array_merge( $targetedMimes, $mimeref['image'] );
break;
case 4: // Video
$targetedMimes = array_merge( $targetedMimes, $mimeref['video'] );
break;
case 5: // Audio
$targetedMimes = array_merge( $targetedMimes, $mimeref['audio'] );
break;
case 6: // Office Documents
$targetedMimes = array_merge( $targetedMimes,
$mimeref['msoffice']
);
break;
case 7: // OpenOffice Documents
$targetedMimes = array_merge( $targetedMimes,
$mimeref['openoffice']
);
break;
case 8: // iWork Documents
$targetedMimes = array_merge( $targetedMimes,
$mimeref['iwork']
);
break;
default: // All Documents (which is stored as zero)
$targetedMimes = array_merge( $targetedMimes,
$mimeref['text'],
$mimeref['application'],
$mimeref['msoffice'],
$mimeref['openoffice'],
$mimeref['wordperfect'],
$mimeref['iwork']
);
break;
}
}
// remove dupes
$targetedMimes = array_unique( $targetedMimes );
return $targetedMimes;
}
/**
* Output the markup for the document content meta box
*
* @param $post
*
* @since 1.0
*/
function document_content_meta_box_markup( $post ) {
$existingContent = get_post_meta( $post->ID, SEARCHWP_PREFIX . 'content', true );
$pdf_metadata = get_post_meta( $post->ID, SEARCHWP_PREFIX . 'pdf_metadata', true );
$supportedMimeTypes = array(
'text/plain',
'text/csv',
'text/tab-separated-values',
'text/calendar',
'text/richtext',
'text/css',
'text/html',
'application/pdf',
'application/rtf',
'application/msword',
'application/vnd.ms-powerpoint',
'application/vnd.ms-write',
'application/vnd.ms-excel',
'application/vnd.ms-access',
'application/vnd.ms-project',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-word.document.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'application/vnd.ms-word.template.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel.sheet.macroEnabled.12',
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'application/vnd.ms-excel.template.macroEnabled.12',
'application/vnd.ms-excel.addin.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.template',
'application/vnd.ms-powerpoint.template.macroEnabled.12',
'application/vnd.ms-powerpoint.addin.macroEnabled.12',
'application/vnd.openxmlformats-officedocument.presentationml.slide',
'application/vnd.ms-powerpoint.slide.macroEnabled.12',
'application/onenote',
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.graphics',
'application/vnd.oasis.opendocument.chart',
'application/vnd.oasis.opendocument.database',
'application/vnd.oasis.opendocument.formula',
'application/wordperfect',
'application/vnd.apple.keynote',
'application/vnd.apple.numbers',
'application/vnd.apple.pages',
);
do_action( 'searchwp_document_content_before', $post );
if ( in_array( $post->post_mime_type, $supportedMimeTypes ) ) : ?>
<?php $file_content_limit = absint( apply_filters( 'searchwp_file_content_limit', 1000000 ) ); ?>
<?php if ( $file_content_limit > strlen( $existingContent ) ) : ?>
<?php wp_nonce_field( 'searchwpdoc', 'searchwp_doc_nonce' ); ?>
<p><?php esc_html_e( 'The content below will be indexed for this file. If you are experiencing unexpected search results, ensure accuracy here.', 'searchwp' ); ?></p>
<!--suppress HtmlFormInputWithoutLabel -->
<textarea style="display:block;width:100%;height:300px;" name="searchwp_doc_content"><?php if ( $existingContent ) { echo esc_textarea( $existingContent ); } ?></textarea>
<div style="display:none !important;overflow:hidden !important;">
<!--suppress HtmlFormInputWithoutLabel -->
<textarea style="display:block;width:100%;height:300px;" name="searchwp_doc_content_original"><?php if ( $existingContent ) { echo esc_textarea( $existingContent ); } ?></textarea>
</div>
<?php else : ?>
<?php
if ( function_exists( 'mb_strlen' ) ) {
$size = mb_strlen( $existingContent, '8bit' );
} else {
$size = strlen( $existingContent );
}
$sample = wordwrap( $existingContent, 1000 );
$sample = explode( "\n", $sample );
$sample = array_slice( $sample, 0, 100 );
$sample = implode( ' ', $sample );
unset( $existingContent );
?>
<p><?php echo wp_kses( sprintf( __( '<strong>NOTE:</strong> This content is too long to display (%s). Here is a sample from the indexed content:', 'searchwp' ), size_format( $size, 2 ) ), array( 'strong' => array() ) ); ?></p>
<!--suppress HtmlFormInputWithoutLabel -->
<textarea style="display:block;width:100%;height:9em;" disabled="disabled"><?php echo esc_textarea( $sample ); ?></textarea>
<p><?php echo wp_kses( __( "To override this limit you must add the following to your theme's <code>functions.php</code>:", 'searchwp' ), array( 'code' => array() ) ); ?></p>
<!--suppress HtmlFormInputWithoutLabel -->
<textarea style="display:block;width:100%;height:8em;" disabled="disabled">function searchwp_file_content_limit( $limit ) {
return <?php echo absint( $size + 100 ); ?>;
}
add_filter( 'searchwp_file_content_limit', 'my_searchwp_file_content_limit' );</textarea>
<?php endif; ?>
<?php
if ( ! empty( $pdf_metadata ) ) {
$this->echo_indexed_pdf_metadata( $pdf_metadata );
}
?>
<?php else : ?>
<p><?php esc_html_e( 'Only plain text files, PDFs, and Office documents are supported at this time.', 'searchwp' ); ?></p>
<?php endif;
do_action( 'searchwp_document_content_after', $post );
}
/**
* Output the PDF metadata that was indexed in the SearchWP File Contents Meta box
*
* @since 2.5
* @param array $pdf_metadata PDF metadata to echo
*/
private function echo_indexed_pdf_metadata( $pdf_metadata ) {
?>
<div class="searchwp-indexed-pdf-metadata">
<h3><?php _e( 'Indexed PDF Metadata', 'searchwp' ); ?></h3>
<table>
<thead>
<tr>
<th><?php _e( 'Key', 'searchwp' ); ?></th>
<th><?php _e( 'Value', 'searchwp' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $pdf_metadata as $key => $val ) : ?>
<tr>
<td><strong><?php echo esc_html( $key ); ?></strong></td>
<td>
<?php
if ( is_array( $val ) ) {
$val = array_map( 'esc_html', $val );
echo implode( '<br />', $val );
} else {
echo esc_html( $val );
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<style type="text/css">
.searchwp-indexed-pdf-metadata {
padding-top:1em;
opacity:0.7;
}
#poststuff .searchwp-indexed-pdf-metadata h3,
.searchwp-indexed-pdf-metadata h3 {
padding-left:0;
padding-bottom:0.5em;
}
.searchwp-indexed-pdf-metadata table {
width:100%;
border-collapse: collapse;
}
.searchwp-indexed-pdf-metadata td {
padding:0.5em 0;
border-top:1px solid #eee;
}
.searchwp-indexed-pdf-metadata table thead {
display:none;
}
</style>
<?php
}
/**
* Callback fired when saving documents, saves document content
*
* @param $post_id
*
* @since 1.0
*/
function document_content_save( $post_id ) {
// check capability
if ( ! isset( $_REQUEST['post_type'] ) ) {
return;
}
if ( 'attachment' == $_REQUEST['post_type'] ) {
if ( ! current_user_can( 'edit_page', $post_id ) ) {
return;
}
}
else {
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
}
// check intent
if ( ! isset( $_POST['searchwp_doc_nonce'] ) || ! wp_verify_nonce( $_POST['searchwp_doc_nonce'], 'searchwpdoc' ) ) {
return;
}
$originalContent = isset( $_POST['searchwp_doc_content_original'] ) ? sanitize_text_field( $_POST['searchwp_doc_content_original'] ) : '';
$editedContent = isset( $_POST['searchwp_doc_content'] ) ? sanitize_text_field( $_POST['searchwp_doc_content'] ) : '';
$alreadySkipped = get_post_meta( $post_id, '_' . SEARCHWP_PREFIX . 'skip_doc_processing', true );
// check to see if the doc content is different than what it was
if ( $alreadySkipped || ( md5( $originalContent ) != md5( $editedContent ) ) ) {
do_action( 'searchwp_log', 'File content was edited by hand, saving' );
$postID = false;
if ( isset( $this->post ) ) {
$postID = $this->post->ID;
} elseif ( is_numeric( $post_id ) ) {
$postID = $post_id;
}
if ( $postID ) {
update_post_meta( $post_id, '_' . SEARCHWP_PREFIX . 'skip_doc_processing', true );
update_post_meta( $post_id, SEARCHWP_PREFIX . 'content', $editedContent );
// TODO: better handling of non-auto-indexed file formats ($this->post is not defined for those attachments)
delete_post_meta( $postID, '_' . SEARCHWP_PREFIX . 'attempts' );
delete_post_meta( $postID, '_' . SEARCHWP_PREFIX . 'skip' );
// allow this post to be indexed on the next pass
$this->purge_post( $postID );
}
}
}
/**
* By default we strip all punctuation from content before indexing it. Unfortunately that level of aggressiveness
* results in the loss of some data (e.g. dates), so this method will allow us to whitelist regex patterns that
* excuse matches from being lost in the sanitization process
*
* @param $content string raw content
* @param bool $buffer whether to include a buffer before and after each whitelisted term
*
* @return array found matches
* @since 1.9
*/
function extract_terms_using_pattern_whitelist( $content, $buffer = true ) {
$content = ' ' . $content . ' ';
$matches = array();
$term_pattern_whitelist = apply_filters( 'searchwp_term_pattern_whitelist', $this->term_pattern_whitelist );
$term_pattern_whitelist = array_unique( $term_pattern_whitelist );
if ( is_array( $term_pattern_whitelist ) && ! empty( $term_pattern_whitelist ) ) {
foreach ( $term_pattern_whitelist as $term_pattern ) {
preg_match_all( $term_pattern, $content, $pattern_matches );
if ( ! empty( $pattern_matches ) ) {
foreach ( $pattern_matches as $pattern_match ) {
if ( is_array( $pattern_match ) && ! empty( $pattern_match ) && ! empty( $content ) ) {
$matches = array_merge( $matches, $pattern_match );
// let the developer remove these matches ASAP to prevent further collisions (but not during indexing, we always want to index multiple matches!)
if ( ! did_action( 'searchwp_indexer_running' ) && apply_filters( 'searchwp_exclusive_regex_matches', false ) ) {
$content = trim( str_ireplace( $matches, ' ', ' ' . $content . ' ' ) );
}
foreach ( $matches as $matches_key => $match ) {
$match_trimmed = trim( $match );
$match_lower = function_exists( 'mb_strtolower' ) ? mb_strtolower( $match_trimmed, 'UTF-8' ) : strtolower( $match_trimmed );
$matches[ $matches_key ] = ' ' . $match_lower . ' '; // add a buffer for whole word matching
}
}
}
}
}
}
// all matches are (usually) buffered with spaces to allow string replacement
$buffer = $buffer ? ' ' : '';
foreach ( $matches as $match_key => $match ) {
$matches[ $match_key ] = $buffer . trim( $match ) . $buffer;
}
$matches = array_unique( $matches );
$matches = array_map( 'sanitize_text_field', $matches );
$matches = array_filter( array_map( 'trim', $matches ), 'strlen' );
return $matches;
}
/**
* Determine a canonical list of enabled post types (as far as search engine configs are concerned)
*
* @since 2.6
*
* @return array
*/
function get_enabled_post_types_across_all_engines() {
$enabled_post_types = array();
foreach ( $this->settings['engines'] as $engine => $post_types ) {
foreach ( $post_types as $post_type => $post_type_settings ) {
if (
( ( isset( $post_type_settings['enabled'] ) && ! empty( $post_type_settings['enabled'] ) )
) && post_type_exists( $post_type ) ) {
$enabled_post_types[] = $post_type;
}
}
}
return array_unique( $enabled_post_types );
}
/**
* Return all IDs that are excluded based on ALL engine rules
*
* e.g. If an ID is excluded from the default engine but NOT excluded from
* a supplemental engine, it should NOT be excluded from the index
*
* @since 2.9.0
*/
function get_post__not_in_across_all_engines( $existing ) {
return $this->get_post_ids_for_rules( $existing, 'exclude' );
}
/**
* Return all IDs that are limited to based on ALL engine rules
*
* e.g. If an ID is limited to from the default engine but NOT limited
* to from a supplemental engine, it should NOT be limited in the index
*
* @since 2.9.0
*/
function get_post__in_across_all_engines( $existing, $post_type = 'post' ) {
$post_type_exists = post_type_exists( $post_type );
if ( ! $post_type_exists ) {
return $existing;
}
return $this->get_post_ids_for_rules( $existing, 'limit_to', $post_type );
}
/**
* Generates a proper tax query that takes into account Rules across all engines
*
* @since 2.9.0
*/
function get_post_type_tax_query_for_rules( $post_type = 'post', $rules = 'exclude' ) {
if ( $rules !== 'exclude' ) {
$rules = 'limit_to';
}
// We're going to return a tax_query that meets our criteria
$tax_query = array();
$post_type_exists = post_type_exists( $post_type );
if ( ! $post_type_exists ) {
return $tax_query;
}
$taxonomies = get_object_taxonomies( $post_type );
if ( empty( $taxonomies ) ) {
return $tax_query;
}
// Loop through post type taxonomies to find rules across all engines for that post type
foreach ( $taxonomies as $taxonomy ) {
$tax_term_ids = array();
foreach ( $this->settings['engines'] as $engine => $post_types ) {
// Does the post type exist in the engine?
if ( ! isset( $post_types[ $post_type ] ) ) {
continue;
}
// Is the post type enabled for this engine?
if ( empty( $post_types[ $post_type ]['enabled'] ) ) {
continue;
}
// This is only applicable if the post type is in fact enabled
$tax_term_ids[ $engine ] = array();
// Is there an applicable rule for this taxonomy for this post type in this engine?
if (
empty( $post_types[ $post_type ]['options'] )
|| empty( $post_types[ $post_type ]['options'][ $rules . '_' . $taxonomy ] )
) {
continue;
}
// Retrieve the term IDs for this taxonomy for this post type for this engine
$term_ids = explode( ',', $post_types[ $post_type ]['options'][ $rules . '_' . $taxonomy ] );
if ( ! is_array( $term_ids ) ) {
$term_ids = array( absint( $term_ids ) );
}
if ( empty( $term_ids ) ) {
continue;
}
$term_ids = array_map( 'absint', $term_ids );
$tax_term_ids[ $engine ] = $term_ids;
}
// We only want IDs that are shared across ALL engines
// If there's only one array element we'll take that
if ( count( $tax_term_ids ) > 1 ) {
$tax_term_ids = call_user_func_array( 'array_intersect', $tax_term_ids );
} else {
$tax_term_ids = array_values( $tax_term_ids );
$tax_term_ids = isset( $tax_term_ids[0] ) ? $tax_term_ids[0] : array();
}
if ( empty( $tax_term_ids ) ) {
continue;
}
$tax_term_ids = array_map( 'absint', $tax_term_ids );
$tax_term_ids = array_unique( $tax_term_ids );
$tax_query[] = array(
'taxonomy' => $taxonomy,
'field' => 'term_id',
'terms' => $tax_term_ids,
'operator' => $rules === 'limit_to' ? 'IN' : 'NOT IN',
);
}
// If we're limiting, we can use an OR relation, but if we're excluding
// we need to use an AND relation so as to accommodate use cases of exclusion
// in one engine but not another.
if ( ! empty( $tax_query ) ) {
$tax_query['relation'] = $rules === 'limit_to' ? 'OR' : 'AND';
}
// Now we have the term IDs across all engines, we need to build our tax_query compatible arg
return $tax_query;
}
/**
* Generates an array of post IDs based on rules across all engines
*
* @since 2.9.0
*/
function get_post_ids_for_rules( $existing_ids, $rules = 'exclude', $only_post_type = false ) {
if ( $rules !== 'exclude' ) {
$rules = 'limit_to';
}
$ids = array();
if ( ! is_array( $existing_ids ) ) {
$existing_ids = explode( ',', $existing_ids );
$existing_ids = array_map( 'absint', $existing_ids );
$existing_ids = array_unique( $existing_ids );
}
foreach ( $this->settings['engines'] as $engine => $post_types ) {
$ids[ $engine ] = array();
foreach ( $post_types as $post_type => $post_type_settings ) {
if ( $only_post_type && $only_post_type !== $post_type ) {
continue;
}
// Exclusion rules allow for comma separated IDs, limit to rules do not
if ( 'exclude' === $rules ) {
// Excluded IDs will be listed as comma separated
if ( isset( $post_type_settings['options']['exclude'] ) && ! empty( $post_type_settings['options']['exclude'] ) ) {
$post_type_excluded_ids = $post_type_settings['options']['exclude'];
// Maybe stored as a comma separated string of integers
$comma_strpos = strpos( $post_type_excluded_ids, ',' );
if ( is_string( $post_type_excluded_ids ) && false !== $comma_strpos ) {
$post_type_excluded_ids = explode( ',', $post_type_excluded_ids );
} else {
if ( is_string( $post_type_excluded_ids ) ) {
$post_type_excluded_ids = array( absint( $post_type_excluded_ids ) );
} else {
$post_type_excluded_ids = array();
}
}
if ( is_array( $post_type_excluded_ids ) && ! empty( $post_type_excluded_ids ) ) {
$post_type_excluded_ids = array_map( 'absint', $post_type_excluded_ids );
$ids[ $engine ] = array_merge( $ids[ $engine ], $post_type_excluded_ids );
}
}
}
// Engines can also exclude by taxonoy term
$taxonomies = get_object_taxonomies( $post_type );
if ( ! is_array( $taxonomies ) || empty( $taxonomies ) ) {
continue;
}
foreach ( $taxonomies as $taxonomy ) {
$taxonomy = get_taxonomy( $taxonomy );
if ( isset( $post_type_settings['options'][ $rules . '_' . $taxonomy->name ] ) ) {
$term_ids = explode( ',', $post_type_settings['options'][ $rules . '_' . $taxonomy->name ] );
if ( ! is_array( $term_ids ) ) {
$term_ids = array( absint( $term_ids ) );
}
if ( empty( $term_ids ) ) {
continue;
}
$term_ids = array_map( 'absint', $term_ids );
// Determine which post(s) have this term
$args = array(
'posts_per_page' => -1,
'fields' => 'ids',
'post_type' => $post_type,
'suppress_filters' => true,
'tax_query' => array(
array(
'taxonomy' => $taxonomy->name,
'field' => 'term_id',
'terms' => $term_ids,
),
)
);
// Media won't be published
if ( 'attachment' == $post_type ) {
$args['post_status'] = 'inherit';
}
// To prevent outside influence of this query we're going to remove all of the filters
// This is only an indexer request so it won't cause any outside influence
if ( apply_filters( 'searchwp_remove_pre_get_posts', true ) ) {
remove_all_actions( 'pre_get_posts' );
remove_all_filters( 'pre_get_posts' );
}
$ids_from_rules_by_taxonomy_term = get_posts( $args );
if ( ! empty( $ids_from_rules_by_taxonomy_term ) ) {
$ids[ $engine ] = array_merge( $ids[ $engine ], $ids_from_rules_by_taxonomy_term );
}
}
}
}
}
// We only want IDs that are shared across ALL engines
if ( count( $ids ) > 1 ) {
$ids = call_user_func_array( 'array_intersect', $ids );
} else {
$ids = $ids['default'];
}
if ( ! is_array( $ids ) ) {
$ids = array();
}
// Now we can merge the common IDs with the existing exclusions
$ids = array_merge( $ids, $existing_ids );
$ids = array_map( 'absint', $ids );
$ids = array_unique( $ids );
return $ids;
}
/**
* Retrieve an array of indexed post type names
*
* @since 2.6.2
* @return array Post type names
*/
function get_indexed_post_types() {
$indexed_post_types = apply_filters( 'searchwp_indexed_post_types', $this->postTypes );
if ( is_array( $indexed_post_types ) ) {
$indexed_post_types = array_merge( $this->postTypes, $indexed_post_types );
$indexed_post_types = array_unique( $indexed_post_types );
}
return $indexed_post_types;
}
/**
* Determine if keyword stemming is supported in this locale
*
* @since 2.6.2
* @return bool
*/
function is_stemming_supported_in_locale() {
$stemmer = new SearchWPStemmer();
$locale = apply_filters( 'searchwp_locale_override', get_locale() );
$language_code = strtolower( substr( $locale, 0, 2 ) );
$supported_codes = $stemmer->get_supported_language_codes();
$supported = ( in_array( $language_code, $supported_codes ) || apply_filters( 'searchwp_keyword_stem_locale', false, $locale ) );
return $supported;
}
/**
* Determine whether the submitted meta key is used in any engine
*
* @since 2.9.0
*/
function is_used_meta_key( $meta_key, $the_post ) {
$used = false;
foreach ( $this->settings['engines'] as $engine => $post_types ) {
foreach ( $post_types as $post_type => $post_type_settings ) {
if ( $post_type !== $the_post->post_type ) {
continue;
}
if ( ! isset( $post_type_settings['enabled'] ) || empty( $post_type_settings['enabled'] ) ) {
continue;
}
if ( empty( $post_type_settings['weights'] ) ) {
continue;
}
if ( empty( $post_type_settings['weights']['cf'] ) ) {
continue;
}
$engine_post_type_meta_keys = array();
$engine_post_type_meta_key_pairs = $post_type_settings['weights']['cf'];
foreach ( $engine_post_type_meta_key_pairs as $engine_post_type_meta_key_pair ) {
$engine_post_type_meta_keys[] = $engine_post_type_meta_key_pair['metakey'];
}
// If an 'Any Custom Field' is added, it means all Custom Fields apply
if (
in_array( 'searchwpcfdefault', $engine_post_type_meta_keys )
|| in_array( 'searchwp cf default', $engine_post_type_meta_keys )
|| in_array( $meta_key, $engine_post_type_meta_keys )
) {
$used = true;
break;
} else {
// Check for LIKE matches to meta key names
foreach ( $engine_post_type_meta_keys as $engine_meta_key ) {
if ( false === strpos( $engine_meta_key, '%' ) ) {
continue;
}
$engine_meta_key_parts = explode( '%', $engine_meta_key );
foreach ( $engine_meta_key_parts as $engine_meta_key_part ) {
if ( ! empty( $engine_meta_key_part ) && false !== strpos( $meta_key, $engine_meta_key_part ) ) {
$used = true;
break;
}
}
if ( $used ) {
break;
}
}
}
}
if ( $used ) {
break;
}
}
$used = apply_filters( 'searchwp_is_used_meta_key', $used, $meta_key );
$used = ! empty( $used );
return $used;
}
/**
* Determine whether the submitted taxonomy is used in any engine
*
* @since 2.9.0
*/
function is_used_taxonomy( $taxonomy ) {
$used = false;
if ( empty( $this->settings['engines'] ) ) {
return $used;
}
foreach ( $this->settings['engines'] as $engine => $post_types ) {
foreach ( $post_types as $post_type => $post_type_settings ) {
if ( ! isset( $post_type_settings['enabled'] ) || empty( $post_type_settings['enabled'] ) ) {
continue;
}
if ( empty( $post_type_settings['weights'] ) ) {
continue;
}
if ( empty( $post_type_settings['weights']['tax'] ) ) {
continue;
}
$engine_post_type_taxonomies = $post_type_settings['weights']['tax'];
if ( ! array_key_exists( $taxonomy, $engine_post_type_taxonomies ) ) {
continue;
}
$weight = intval( $engine_post_type_taxonomies[ $taxonomy ] );
if ( $weight > 0 ) {
$used = true;
break;
}
}
if ( $used ) {
break;
}
}
$used = apply_filters( 'searchwp_is_used_taxonomy', $used, $taxonomy );
$used = ! empty( $used );
return $used;
}
/**
* Returns a default configuration for post type
*
* @since 2.9
*/
public function get_default_config_for_post_type( $post_type ) {
if ( ! post_type_exists( $post_type ) ) {
return array();
}
// Bare essentials, disabled
$config = array(
'enabled' => false,
'weights' => array(
'cf' => new stdClass(),
'tax' => array(),
),
'options' => array(
'exclude' => '',
'attribute_to' => '',
'parent' => false,
),
);
// Weights depend on supported features; set the defaults
$supports = searchwp_get_supports_for_post_type( $post_type );
$max_weight = apply_filters( 'searchwp_weight_max', 100 );
foreach ( $supports as $supported => $label ) {
switch ( $supported ) {
case 'title':
$weight = absint( $max_weight ) * 0.8;
break;
case 'content':
$weight = absint( $max_weight ) * 0.05;
break;
case 'slug':
$weight = absint( $max_weight ) * 0.6;
break;
case 'excerpt':
$weight = absint( $max_weight ) * 0.4;
break;
case 'comments':
$weight = absint( $max_weight ) * 0.01;
break;
default:
$weight = absint( $max_weight ) * 0.01;
}
$config['weights'][ $supported ] = $weight;
}
// Set up placeholders for taxonomy weights and taxonomy excludes, they're expected
$taxonomies = get_object_taxonomies( $post_type );
foreach ( $taxonomies as $taxonomy ) {
$config['weights']['tax'][ $taxonomy ] = 0;
}
return $config;
}
/**
* Deprecated functions — most are internal and only deprecated because camelCase
*/
// @codingStandardsIgnoreStart
/**
* @deprecated as of 2.5.7
*/
function checkDatabaseEnvironment() {
$this->check_database_environment();
}
/**
* @deprecated as of 2.5.7
*/
function setupPurgeQueue() {
$this->setup_purge_queue();
}
/**
* @deprecated as of 2.5.7
*/
function getPid() {
return $this->get_pid();
}
/**
* @deprecated as of 2.5.7
*/
function maybeOutputDebug() {
$this->maybe_output_debug();
}
/**
* @deprecated as of 2.5.7
*
* @param $name
* @param $id
* @param bool $href
*/
function adminBarAddRootMenu( $name, $id, $href = false ) {
$this->admin_bar_add_root_menu( $name, $id, $href );
}
/**
* @deprecated as of 2.5.7
*
* @param $name
* @param $link
* @param $root_menu
* @param $id
* @param bool $meta
*
* @internal param bool $href
*/
function adminBarAddSubMenu( $name, $link, $root_menu, $id, $meta = false ) {
$this->admin_bar_add_sub_menu( $name, $link, $root_menu, $id, $meta );
}
/**
* @deprecated as of 2.5.7
*/
function adminBarMenu() {
$this->admin_bar_menu();
}
/**
* @deprecated as of 2.5.7
*
* @param $post_id
* @param bool $timeDiff
*/
function getLastIndexedTime( $post_id, $timeDiff = false ) {
$this->get_last_indexed_time( $post_id, $timeDiff );
}
/**
* @deprecated as of 2.5.7
*/
function indexerPause() {
$this->indexer_pause();
}
/**
* @deprecated as of 2.5.7
*/
function indexerUnpause() {
$this->indexer_unpause();
}
/**
* @deprecated as of 2.5.7
*/
function checkIfPaused() {
$this->check_if_paused();
}
/**
* @deprecated as of 2.5.7
*/
function validateDatabaseEnvironment() {
$this->validate_database_environment();
}
/**
* @deprecated as of 2.5.7
*/
function processUpdates() {
$this->process_updates();
}
/**
* @deprecated as of 2.5.7
*
* @param null $plugin_file
* @param $plugin_data
* @param null $status
*/
function pluginRow( $plugin_file = null, $plugin_data, $status = null ) {
$this->plugin_row( $plugin_file, $plugin_data, $status );
}
/**
* @deprecated as of 2.5.7
*/
function triggerIndex() {
$this->trigger_index();
}
/**
* @deprecated as of 2.5.7
*
* @param $query
*
* @return mixed
*/
function checkForMainQuery( $query ) {
return $this->check_for_main_query( $query );
}
/**
* @deprecated as of 2.5.7
*
* @param $engineName
*
* @return bool
*/
function isValidEngine( $engineName ) {
return $this->is_valid_engine( $engineName );
}
/**
* @deprecated as of 2.5.7
*
* @param $terms
* @param string $engine
*
* @return array
*/
function sanitizeTerms( $terms, $engine = 'default' ) {
return $this->sanitize_terms( $terms, $engine );
}
/**
* @deprecated as of 2.5.7
*
* @param $query
*
* @return bool|string
*/
function maybeCancelWpQuery( $query ) {
return $this->maybe_cancel_wp_query( $query );
}
/**
* @deprecated as of 2.5.7
*
* @param $posts
*
* @return array
*/
function wpSearch( $posts ) {
return $this->wp_search( $posts );
}
/**
* @deprecated as of 2.5.7
*/
function resetStats() {
$this->reset_stats();
}
/**
* @deprecated as of 2.5.7
*/
function purgeIndex() {
$this->purge_index();
}
/**
* @deprecated as of 2.5.7
*/
function getEnvironment() {
return false;
}
/**
* @deprecated as of 2.5.7
*/
function showErroneousPosts() {
$this->show_erroneous_posts();
}
/**
* @deprecated as of 2.5.7
*
* @param $input
*
* @return mixed
*/
function validateSettings( $input ) {
return $this->validate_settings( $input );
}
/**
* @deprecated as of 2.5.7
*
* @param $settings
*
* @return array
*/
function sanitizeEngineSettings( $settings ) {
return $this->sanitize_engine_settings( $settings );
}
/**
* @deprecated as of 2.5.7
*
* @param $post_id
*/
function purgePostViaEdit( $post_id ) {
$this->purge_post_via_edit( $post_id );
}
/**
* @deprecated as of 2.5.7
*
* @param $post_id
*/
function purgePost( $post_id ) {
$this->purge_post( $post_id );
}
/**
* @deprecated as of 2.5.7
*
* @param $id
*/
function purgePostViaComment( $id ) {
$this->purge_post_via_comment( $id );
}
/**
* @deprecated as of 2.5.7
*
* @param $object_id
* @param $terms
* @param $tt_ids
* @param $taxonomy
* @param $append
* @param $old_tt_ids
*/
function purgePostViaTerm( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
$this->purge_post_via_term( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids );
}
/**
* @deprecated as of 2.5.7
*/
/** @noinspection PhpUnusedPrivateMethodInspection */
private function triggerReindex() {
$this->trigger_reindex();
}
// @codingStandardsIgnoreEnd
}
/**
* Deactivation routine
*/
if ( ! function_exists( 'swp_deactivate' ) ) {
function swp_deactivate() {
// remove cron jobs
$swp_maintenance_timestamp = wp_next_scheduled( 'swp_maintenance' );
if ( $swp_maintenance_timestamp ) {
wp_unschedule_event( $swp_maintenance_timestamp, 'swp_maintenance' );
}
$swp_indexer_timestamp = wp_next_scheduled( 'swp_indexer' );
if ( $swp_indexer_timestamp ) {
wp_unschedule_event( $swp_indexer_timestamp, 'swp_indexer' );
}
// remove database validation flag
searchwp_set_setting( 'valid_db_environment', false );
}
}
register_deactivation_hook( __FILE__, 'swp_deactivate' );
if ( ! function_exists( 'swp_init' ) ) {
/**
* The one true SearchWP
*
* @return SearchWP SearchWP singleton
* @since 1.0
*/
function swp_init() {
global $searchwp;
if ( is_admin() || apply_filters( 'searchwp_init', true ) ) {
$searchwp = SearchWP::instance();
}
if ( isset( $_GET['swpjumpstart'] ) ) {
$waiting = searchwp_get_option( 'waiting' );
if ( ! $waiting ) {
searchwp_wake_up_indexer();
}
}
return $searchwp;
}
}
// @codingStandardsIgnoreStart
if ( ! function_exists( 'SWP' ) ) {
/**
* Set up a reference function instead of using a global
* if the function already exists DO NOT INIT
*
* @since 2.3
*
* @return SearchWP
*/
function SWP() {
return SearchWP::instance();
}
// initialize SearchWP Singleton
swp_init();
add_action( 'wp_ajax_swp_progress', 'searchwp_get_indexer_progress' );
add_action( 'wp_ajax_swp_conflict', 'swp_dismiss_filter_conflict' );
}
// @codingStandardsIgnoreEnd