init_db_name();
$this->db_file = $this->get_db_path();
$this->init_db();
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_init', array($this, 'check_db_security'));
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_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 init_db_name() {
$this->db_name = get_option('admin_compass_db_name');
if (!$this->db_name) {
$this->db_name = $this->generate_db_name();
update_option('admin_compass_db_name', $this->db_name);
}
}
private function generate_db_name() {
$random_string = bin2hex(random_bytes(16)); // 32 character random string
return 'admin_compass_' . $random_string . '.db';
}
private function get_db_path() {
// Store the database one level above the WordPress root
return WP_CONTENT_DIR . '/' . $this->db_name;
}
private function remove_db_file() {
if (file_exists($this->db_file)) {
// Close the database connection if it's open
if ($this->db) {
$this->db->close();
}
// Delete the file
unlink($this->db_file);
}
// Remove the database name from options
delete_option('admin_compass_db_name');
}
public function activate() {
// Check if we need to rename an existing database
$old_db_file = ABSPATH . '../admin-compass.db';
if (file_exists($old_db_file) && !file_exists($this->db_file)) {
rename($old_db_file, $this->db_file);
}
// Ensure the database file has the correct permissions
if (file_exists($this->db_file)) {
chmod($this->db_file, 0600);
}
$this->create_tables();
set_transient( 'admin_compass_reindex_admin_menu', true);
$this->rebuild_index();
}
public function deactivate() {
wp_clear_scheduled_hook('admin_compass_rebuild_index');
$this->remove_db_file();
}
private function init_db() {
if (!class_exists('SQLite3')) {
add_action('admin_notices', function() {
echo '
Admin Compass requires SQLite3 PHP extension. Please install it and try again.
';
});
return;
}
$this->db = new SQLite3($this->db_file);
$this->db->exec('PRAGMA journal_mode = WAL;');
}
private function create_tables() {
$this->db->exec("
CREATE TABLE IF NOT EXISTS search_index (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item_id INTEGER,
item_type TEXT,
thumbnail_url TEXT,
title TEXT,
content TEXT,
edit_url TEXT
);
CREATE INDEX IF NOT EXISTS idx_title ON search_index(title);
CREATE INDEX IF NOT EXISTS idx_content ON search_index(content);
");
}
public function rebuild_index() {
$this->db->exec("DELETE FROM search_index WHERE item_type != 'admin_page'");
// Index posts and pages
$posts = get_posts(array(
'post_type' => array('post', 'page', 'attachment'),
'posts_per_page' => 300,
'post_status' => null,
'post_parent' => null,
));
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);
}
$this->add_to_index($post->ID, $post->post_type, $post->post_title, $content, $this->get_edit_post_link($post, 'raw'), get_the_post_thumbnail_url($post));
}
}
public function admin_menu() {
global $menu, $submenu;
if (get_transient( 'admin_compass_reindex_admin_menu') !== "1") {
return;
}
delete_transient('admin_compass_reindex_admin_menu');
$this->db->exec("DELETE FROM search_index WHERE item_type = 'admin_page'");
foreach ($menu as $menu_item) {
if (empty($menu_item[0])) continue;
$menu_title = wp_strip_all_tags($menu_item[0]);
$menu_url = $menu_item[2];
// Index main menu item
$this->add_to_index(0, 'admin_page', $menu_title, "Navigate to $menu_title admin page", admin_url($menu_url), null);
// Index submenu items
if (isset($submenu[$menu_url])) {
foreach ($submenu[$menu_url] as $submenu_item) {
$submenu_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, 'admin_page', "$menu_title - $submenu_title", "Navigate to $submenu_title under $menu_title", admin_url($submenu_url), null);
}
}
}
}
private function add_to_index($item_id, $item_type, $title, $content, $edit_url, $thumbnail_url) {
$stmt = $this->db->prepare("INSERT INTO search_index (item_id, item_type, title, content, edit_url, thumbnail_url) VALUES (:item_id, :item_type, :title, :content, :edit_url, :thumbnail_url)");
$stmt->bindValue(':item_id', $item_id, SQLITE3_INTEGER);
$stmt->bindValue(':item_type', $item_type, SQLITE3_TEXT);
$stmt->bindValue(':title', $title, SQLITE3_TEXT);
$stmt->bindValue(':content', $content, SQLITE3_TEXT);
$stmt->bindValue(':edit_url', $edit_url, SQLITE3_TEXT);
$stmt->bindValue(':thumbnail_url', $thumbnail_url, SQLITE3_TEXT);
$stmt->execute();
}
public function update_index_on_save($post_id, $post, $update) {
$this->remove_from_index($post_id);
$this->add_to_index($post_id, $post->post_type, $post->post_title, $post->post_content, $this->get_edit_post_link($post, 'raw'), get_the_post_thumbnail_url($post));
}
public function remove_from_index($item_id) {
$stmt = $this->db->prepare("DELETE FROM search_index WHERE item_id = :item_id");
$stmt->bindValue(':item_id', $item_id, SQLITE3_INTEGER);
$stmt->execute();
}
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 + Shift + F)',
'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']));
$query = str_replace(' ', '%', $query);
$limit = 10;
$results = array();
$search_query = $this->db->prepare("
SELECT item_id, item_type, title, edit_url, thumbnail_url
FROM search_index
WHERE title LIKE :query OR content LIKE :query
ORDER BY
CASE
WHEN title LIKE :query THEN 1
WHEN content LIKE :query THEN 2
END
LIMIT 15
");
$search_query->bindValue(':query', '%' . $query . '%', SQLITE3_TEXT);
$result = $search_query->execute();
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$results[] = array(
'id' => $row['item_id'],
'title' => $row['title'],
'type' => $row['item_type'],
'edit_url' => $row['edit_url'],
'thumbnail_url' => $row['thumbnail_url'],
);
}
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');
}
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 );
}
public function check_db_security() {
// Check file permissions
if (file_exists($this->db_file)) {
$perms = fileperms($this->db_file);
if (($perms & 0777) !== 0600) {
chmod($this->db_file, 0600);
add_action('admin_notices', function() {
echo 'Admin Compass: The database file permissions have been corrected to 0600.
';
});
}
}
$this->create_htaccess();
$this->check_db_accessibility();
}
private function create_htaccess() {
$htaccess_file = dirname($this->db_file) . '/.htaccess';
if (!file_exists($htaccess_file)) {
$htaccess_content = "
Order allow,deny
Deny from all
";
file_put_contents($htaccess_file, $htaccess_content);
}
}
private function check_db_accessibility() {
$db_url = str_replace(ABSPATH, get_site_url() . '/', $this->db_file);
$response = wp_remote_head($db_url);
if (!is_wp_error($response) && $response['response']['code'] !== 403) {
add_action('admin_notices', function() {
echo 'Warning: Your Admin Compass database file may be publicly accessible. Please check your server configuration.
';
});
}
}
}
$admin_compass = new admin_compass();