table_name = $wpdb->prefix . 'admin_compass_search_index';
add_action('admin_bar_menu', array($this, 'add_search_icon'), 999);
add_action('admin_footer', array($this, 'add_search_modal'));
add_action('wp_footer', array($this, 'add_search_modal'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
add_action('save_post', array($this, 'update_index_on_save'), 10, 3);
add_action('delete_post', array($this, 'remove_from_index'));
add_action('wp_ajax_admin_compass_search', array($this, 'admin_compass_ajax_handler'));
add_action('wp_ajax_nopriv_admin_compass_search', array($this, 'admin_compass_ajax_handler'));
add_action('wp_ajax_admin_compass_check_indexing', array($this, 'check_indexing_status'));
add_filter('plugin_row_meta', array($this, 'add_plugin_meta_links'), 10, 4);
add_action('wp_ajax_admin_compass_reindex', array($this, 'schedule_background_job'));
add_action('admin_menu', array($this, 'admin_menu'));
// Schedule index rebuild
if (!wp_next_scheduled('admin_compass_rebuild_index')) {
wp_schedule_event(time(), 'daily', 'admin_compass_rebuild_index');
}
add_action('admin_compass_rebuild_index', array($this, 'rebuild_index'));
register_activation_hook(__FILE__, array($this, 'activate'));
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
}
private function drop_table() {
global $wpdb;
$wpdb->query("DROP TABLE IF EXISTS {$this->table_name}");
}
public function update() {
$current_version = get_option('admin_compass_version', '0.0.0');
if ($current_version !== '0.0.0' && version_compare($current_version, ADMIN_COMPASS_VERSION, '<')) {
// Drop and recreate the table for updates
$this->drop_table();
$this->create_tables();
$this->rebuild_index();
set_transient( 'admin_compass_reindex_admin_menu', true);
// Update the stored version number
update_option('admin_compass_version', ADMIN_COMPASS_VERSION);
// Add an admin notice about the update
add_action('admin_notices', function() {
echo '
';
echo '
Admin Compass has been updated to version ' . esc_html(ADMIN_COMPASS_VERSION) . '. The search index has been rebuilt.
';
echo '
';
});
}
}
public function activate() {
$this->create_tables();
set_transient( 'admin_compass_reindex_admin_menu', true);
$this->rebuild_index();
update_option('admin_compass_version', ADMIN_COMPASS_VERSION);
}
public function deactivate() {
wp_clear_scheduled_hook('admin_compass_rebuild_index');
// Clear indexing state if deactivated during indexing
delete_option('admin_compass_indexing_in_progress');
delete_option('admin_compass_indexing_started');
}
private function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$this->table_name} (
id bigint(20) NOT NULL AUTO_INCREMENT,
item_id bigint(20) NOT NULL,
item_type varchar(50) NOT NULL,
thumbnail_url text,
title text NOT NULL,
content longtext,
edit_url text NOT NULL,
date_modified datetime DEFAULT NULL,
date_created datetime DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_title (title(191)),
KEY idx_content (content(191)),
KEY idx_item_type (item_type),
KEY idx_combined (title(191), content(191)),
KEY idx_date_modified (date_modified),
KEY idx_date_created (date_created),
UNIQUE KEY idx_item_unique (item_id, item_type)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
}
public function rebuild_index() {
// Set indexing state
update_option('admin_compass_indexing_in_progress', true);
update_option('admin_compass_indexing_started', time());
global $wpdb;
$wpdb->query($wpdb->prepare("DELETE FROM {$this->table_name} WHERE item_type != %s", 'settings'));
// Index posts and pages in batches
$post_types = array('post', 'page', 'attachment', 'product');
$batch_size = 100;
$total_indexed = 0;
foreach ($post_types as $post_type) {
$offset = 0;
do {
$posts = get_posts(array(
'post_type' => $post_type,
'posts_per_page' => $batch_size,
'offset' => $offset,
'post_status' => array('publish', 'draft', 'private'),
));
if (empty($posts)) {
break;
}
foreach ($posts as $post) {
$content = $post->post_content;
if ($post->post_type === 'attachment') {
$content .= ' ' . $post->post_title . ' ' . $post->post_name . ' ' . get_post_meta($post->ID, '_wp_attachment_image_alt', true);
}
if ($post->post_type === 'product' && function_exists('wc_get_product')) {
$product = wc_get_product($post->ID);
if ($product) {
$content .= ' ' . $product->get_sku() . ' ' . $product->get_price();
}
}
$edit_url = $this->get_edit_post_link($post, 'raw');
if (!$edit_url) {
$edit_url = admin_url('post.php?post=' . $post->ID . '&action=edit');
}
$this->add_to_index($post->ID, $post->post_type, $post->post_title, $content, $edit_url, get_the_post_thumbnail_url($post), $post->post_modified, $post->post_date, true);
$total_indexed++;
}
$offset += $batch_size;
// Prevent timeout
if (connection_status() != CONNECTION_NORMAL) {
break 2;
}
} while (count($posts) == $batch_size);
}
// Index orders in batches
if (class_exists('WC_Order_Query') && function_exists('wc_get_order')) {
$batch_size = 50; // Smaller batch for orders as they're more complex
$offset = 0;
$processed_ids = array(); // Track processed IDs to avoid duplicates
do {
$query = new WC_Order_Query(array(
'limit' => $batch_size,
'offset' => $offset,
'return' => 'ids',
'type' => 'shop_order', // Exclude refunds from the query
'orderby' => 'ID',
'order' => 'ASC',
'paginate' => false
));
$order_ids = $query->get_orders();
if (empty($order_ids)) {
break;
}
foreach ($order_ids as $order_id) {
// Skip if already processed in this batch run
if (in_array($order_id, $processed_ids)) {
continue;
}
$processed_ids[] = $order_id;
$order = wc_get_order($order_id);
if ($order) {
// Skip refunds as they don't have billing/shipping methods
if ($order->get_type() === 'shop_order_refund') {
continue;
}
// Include comprehensive order data for search
$customer_data = array(
$order->get_billing_first_name(),
$order->get_billing_last_name(),
$order->get_billing_email(),
$order->get_billing_phone(),
$order->get_billing_company(),
$order->get_shipping_first_name(),
$order->get_shipping_last_name(),
$order->get_shipping_company()
);
$content = sprintf(
'Order #%s %s %s %s %s %s %s',
$order->get_order_number(),
$order->get_formatted_billing_full_name(),
$order->get_billing_email(),
$order->get_total(),
$order->get_status(),
$order->get_billing_phone(),
implode(' ', array_filter($customer_data))
);
$this->add_to_index($order_id, 'shop_order', 'Order #' . $order->get_order_number(), $content, $this->get_edit_order_link($order_id), null, $order->get_date_modified()->format('Y-m-d H:i:s'), $order->get_date_created()->format('Y-m-d H:i:s'), true);
}
}
$offset += $batch_size;
// Prevent timeout
if (connection_status() != CONNECTION_NORMAL) {
break;
}
} while (count($order_ids) == $batch_size);
}
// Clear indexing state
delete_option('admin_compass_indexing_in_progress');
delete_option('admin_compass_indexing_started');
}
public function clean_admin_menu_title($title) {
$title = preg_replace('/[0-9]+/', '', $title);
$comments = __( 'Comments' );
if (strpos($title, $comments) === 0) {
return $comments;
}
return $title;
}
public function admin_menu() {
global $menu, $submenu;
if (!get_transient( 'admin_compass_reindex_admin_menu')) {
return;
}
delete_transient('admin_compass_reindex_admin_menu');
global $wpdb;
$wpdb->delete($this->table_name, array('item_type' => 'settings'));
foreach ($menu as $menu_item) {
if (empty($menu_item[0])) continue;
$menu_title = $this->clean_admin_menu_title(wp_strip_all_tags($menu_item[0]));
$menu_url = $menu_item[2];
// Index main menu item
$this->add_to_index(0, 'settings', $menu_title, "Navigate to $menu_title admin page", admin_url($menu_url), null, null, null, true);
// Index submenu items
if (isset($submenu[$menu_url])) {
foreach ($submenu[$menu_url] as $submenu_item) {
$submenu_title = $this->clean_admin_menu_title(wp_strip_all_tags($submenu_item[0]));
$submenu_url = $submenu_item[2];
// Check if it's a custom plugin page
if (strpos($submenu_url, 'php') === false) {
$submenu_url = $menu_url . '?page=' . $submenu_url;
}
$this->add_to_index(0, 'settings', "$menu_title - $submenu_title", "Navigate to $submenu_title under $menu_title", admin_url($submenu_url), null, null, null, true);
}
}
}
}
private function add_to_index($item_id, $item_type, $title, $content, $edit_url, $thumbnail_url, $date_modified = null, $date_created = null, $force_insert = false) {
global $wpdb;
if ($force_insert) {
// Force insert during rebuild (we already deleted old entries)
$wpdb->insert(
$this->table_name,
array(
'item_id' => $item_id,
'item_type' => $item_type,
'title' => $title,
'content' => $content,
'edit_url' => $edit_url,
'thumbnail_url' => $thumbnail_url,
'date_modified' => $date_modified,
'date_created' => $date_created
),
array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s')
);
} else {
// Use INSERT ... ON DUPLICATE KEY UPDATE for better performance
$wpdb->query($wpdb->prepare(
"INSERT INTO {$this->table_name}
(item_id, item_type, title, content, edit_url, thumbnail_url, date_modified, date_created)
VALUES (%d, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
title = VALUES(title),
content = VALUES(content),
edit_url = VALUES(edit_url),
thumbnail_url = VALUES(thumbnail_url),
date_modified = VALUES(date_modified),
date_created = VALUES(date_created)",
$item_id,
$item_type,
$title,
$content,
$edit_url,
$thumbnail_url,
$date_modified,
$date_created
));
}
}
public function update_index_on_save($post_id, $post, $update) {
$this->remove_from_index($post_id);
$edit_url = $this->get_edit_post_link($post, 'raw');
if (!$edit_url) {
$edit_url = admin_url('post.php?post=' . $post_id . '&action=edit');
}
$this->add_to_index($post_id, $post->post_type, $post->post_title, $post->post_content, $edit_url, get_the_post_thumbnail_url($post), $post->post_modified, $post->post_date);
}
public function remove_from_index($item_id) {
global $wpdb;
$wpdb->delete($this->table_name, array('item_id' => $item_id), array('%d'));
}
public function add_search_icon($wp_admin_bar) {
if (!current_user_can('manage_options')) {
return;
}
$wp_admin_bar->add_node(array(
'id' => 'admin-compass',
'title' => '',
'href' => '#',
'meta' => array(
'title' => 'Admin Compass Search (Ctrl+K or Cmd+K)',
'class' => 'admin-compass-icon'
),
));
}
public function add_search_modal() {
if (!current_user_can('manage_options')) {
return;
}
?>
admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('admin_compass_nonce'),
));
}
public function admin_compass_ajax_handler() {
check_ajax_referer('admin_compass_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
if (empty($_POST['query'])) {
return wp_send_json_success([]);
}
$query = sanitize_text_field(wp_unslash($_POST['query']));
$results = array();
global $wpdb;
$search_term = '%' . $wpdb->esc_like($query) . '%';
$results_data = $wpdb->get_results(
$wpdb->prepare(
"SELECT item_id, item_type, title, edit_url, thumbnail_url, content, date_modified, date_created
FROM {$this->table_name}
WHERE title LIKE %s OR content LIKE %s
ORDER BY
CASE
WHEN title LIKE %s THEN 1
ELSE 2
END,
COALESCE(date_modified, date_created) DESC,
title
LIMIT 15",
$search_term,
$search_term,
'%' . $wpdb->esc_like($query) . '%'
),
ARRAY_A
);
foreach ($results_data as $row) {
// Generate preview excerpt
$preview = '';
if (!empty($row['content'])) {
$preview = wp_strip_all_tags($row['content']);
$preview = substr($preview, 0, 120);
if (strlen($row['content']) > 120) {
$preview .= '...';
}
}
$results[] = array(
'id' => $row['item_id'],
'title' => $row['title'],
'type' => $row['item_type'],
'edit_url' => $row['edit_url'],
'thumbnail_url' => $row['thumbnail_url'],
'preview' => $preview,
);
}
wp_send_json_success($results);
}
function add_plugin_meta_links($meta, $file, $data, $status) {
$plugin_file = plugin_basename(__FILE__);
if ($file == $plugin_file) {
$new_meta = [
'Rebuild index',
];
$meta = array_merge($meta, $new_meta);
}
return $meta;
}
function schedule_background_job() {
check_ajax_referer('admin_compass_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
set_transient( 'admin_compass_reindex_admin_menu', true);
wp_schedule_single_event(time(), 'admin_compass_rebuild_index');
wp_send_json_success('Background job scheduled');
}
public function check_indexing_status() {
check_ajax_referer('admin_compass_nonce', 'nonce');
$is_indexing = get_option('admin_compass_indexing_in_progress', false);
$indexing_started = get_option('admin_compass_indexing_started', 0);
$response = array(
'is_indexing' => $is_indexing,
'started_at' => $indexing_started,
'elapsed_time' => $is_indexing ? (time() - $indexing_started) : 0
);
wp_send_json_success($response);
}
function get_edit_post_link( $post = 0, $context = 'display' ) {
$post = get_post( $post );
if ( ! $post ) {
return;
}
if ( 'revision' === $post->post_type ) {
$action = '';
} elseif ( 'display' === $context ) {
$action = '&action=edit';
} else {
$action = '&action=edit';
}
$post_type_object = get_post_type_object( $post->post_type );
if ( ! $post_type_object ) {
return;
}
$link = '';
if ( 'wp_template' === $post->post_type || 'wp_template_part' === $post->post_type ) {
$slug = urlencode( get_stylesheet() . '//' . $post->post_name );
$link = admin_url( sprintf( $post_type_object->_edit_link, $post->post_type, $slug ) );
} elseif ( 'wp_navigation' === $post->post_type ) {
$link = admin_url( sprintf( $post_type_object->_edit_link, (string) $post->ID ) );
} elseif ( $post_type_object->_edit_link ) {
$link = admin_url( sprintf( $post_type_object->_edit_link . $action, $post->ID ) );
}
/**
* Filters the post edit link.
*
* @since 2.3.0
*
* @param string $link The edit link.
* @param int $post_id Post ID.
* @param string $context The link context. If set to 'display' then ampersands
* are encoded.
*/
return apply_filters( 'get_edit_post_link', $link, $post->ID, $context );
}
function get_edit_order_link($order_id) {
if (!function_exists('wc_get_order')) {
return admin_url('edit.php?post_type=shop_order');
}
return admin_url('post.php?post=' . $order_id . '&action=edit');
}
}
$admin_compass = new admin_compass();
$admin_compass->update();