ensure_tables(); } public function admin_enqueue_styles($hook) { // Only load on our plugin pages if (strpos($hook, 'ai-404-redirect') !== false || $hook === 'toplevel_page_ai-404-redirect') { wp_enqueue_style( 'ai-404-redirect-admin', plugin_dir_url(__FILE__) . 'assets/admin-style.css', array(), AI_404_REDIRECT_VERSION ); } } public function activate() { $this->create_tables(); // Store version for future upgrades add_option('ai_404_redirect_version', AI_404_REDIRECT_VERSION); // Flush rewrite rules to ensure proper URL handling flush_rewrite_rules(); } public function deactivate() { // Flush rewrite rules on deactivation flush_rewrite_rules(); } private function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // Create 404 hits table $hits_table = $wpdb->prefix . 'ai_404_redirect_hits'; $hits_sql = "CREATE TABLE $hits_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, url varchar(500) NOT NULL, first_hit datetime DEFAULT CURRENT_TIMESTAMP, hit_count int DEFAULT 1, last_hit datetime DEFAULT CURRENT_TIMESTAMP, redirected_to varchar(500), user_agent text, referer varchar(500), PRIMARY KEY (id), UNIQUE KEY url (url), KEY first_hit (first_hit) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($hits_sql); // Create manual redirects table $redirects_table = $wpdb->prefix . 'ai_404_manual_redirects'; $redirects_sql = "CREATE TABLE $redirects_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, from_url varchar(500) NOT NULL, to_url varchar(500) NOT NULL, redirect_type int DEFAULT 302, is_active tinyint(1) DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, hit_count int DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY from_url (from_url), KEY is_active (is_active) ) $charset_collate;"; dbDelta($redirects_sql); } private function ensure_tables() { global $wpdb; // Check if manual redirects table exists $manual_table = $wpdb->prefix . 'ai_404_manual_redirects'; $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $manual_table)) === $manual_table; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching if (!$table_exists) { $this->create_tables(); } } public function handle_404() { if (is_404()) { $requested_url = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''; if (empty($requested_url)) { return; } // Track 404 hit $this->track_404_hit($requested_url); // Check for manual redirect first $manual_redirect = $this->get_manual_redirect($requested_url); if ($manual_redirect) { // Get final destination to prevent redirect chains $final_destination = $this->resolve_redirect_chain($manual_redirect['to_url']); $this->increment_manual_redirect_count($manual_redirect['id']); $redirect_url = $this->get_full_redirect_url($final_destination); $this->update_404_redirect($requested_url, $final_destination); wp_redirect($redirect_url, $manual_redirect['redirect_type']); exit; } // Check if we already have a cached redirect for this URL $cached_redirect = $this->get_cached_404_redirect($requested_url); if ($cached_redirect) { // Get final destination to prevent redirect chains $final_destination = $this->resolve_redirect_chain($cached_redirect); // Verify the final redirect target still exists if ($this->verify_redirect_target($final_destination)) { // Update cache if destination changed due to chain resolution if ($final_destination !== $cached_redirect) { $this->update_404_redirect($requested_url, $final_destination); } $redirect_url = home_url($final_destination); wp_redirect($redirect_url, 302); exit; } else { $this->clear_404_redirect_cache($requested_url); } } // If no manual or cached redirect, use AI $best_match = $this->find_best_match($requested_url); if ($best_match && $best_match !== $requested_url) { // Update redirect info before redirecting $this->update_404_redirect($requested_url, $best_match); // Force database write before redirect global $wpdb; if (function_exists('wp_cache_flush')) { wp_cache_flush(); } $redirect_url = home_url($best_match); wp_redirect($redirect_url, 302); exit; } } } private function find_best_match($requested_url) { $available_routes = $this->get_available_routes(); if (empty($available_routes)) { return null; } $ai_response = $this->query_ai($requested_url, $available_routes); if ($ai_response === null) { return null; } return $this->parse_ai_response($ai_response, $available_routes); } private function get_available_routes() { $routes = array(); // Add homepage $routes[] = home_url('/'); // Get all published posts and pages $posts = get_posts(array( 'post_type' => array('post', 'page'), 'post_status' => 'publish', 'numberposts' => -1 )); foreach ($posts as $post) { $routes[] = get_permalink($post->ID); } // Get category and tag archives $categories = get_categories(); foreach ($categories as $category) { $routes[] = get_category_link($category->term_id); } $tags = get_tags(); foreach ($tags as $tag) { $routes[] = get_tag_link($tag->term_id); } // Remove home URL from routes to get relative paths and clean up URLs $home_url = home_url(); $routes = array_map(function($route) use ($home_url) { $relative_path = str_replace($home_url, '', $route); // Remove /index.php/ if present $relative_path = str_replace('/index.php/', '/', $relative_path); // Ensure path starts with / if ($relative_path && $relative_path[0] !== '/') { $relative_path = '/' . $relative_path; } // If empty path, it's homepage if (empty($relative_path) || $relative_path === '/') { return '/'; } return $relative_path; }, $routes); return array_unique($routes); } private function query_ai($requested_url, $available_routes) { $prompt = "A user tried to access: {$requested_url}\n\n"; $prompt .= "Available routes:\n" . implode("\n", $available_routes) . "\n\n"; $prompt .= "Find the most relevant and related route from the available routes. Be creative and consider:\n"; $prompt .= "- Similar topics (e.g., 'rocket' relates to 'spacex', 'space', 'nasa')\n"; $prompt .= "- Related keywords and synonyms\n"; $prompt .= "- Conceptual connections\n"; $prompt .= "- Partial matches and variations\n"; $prompt .= "Return only the exact route path from the list above, nothing else."; $data = array( 'model' => $this->api_model, 'messages' => array( array( 'role' => 'user', 'content' => $prompt ) ), 'max_tokens' => 100, 'temperature' => $this->temperature ); $args = array( 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $this->api_key ), 'body' => json_encode($data), 'timeout' => 30 ); $response = wp_remote_post($this->api_endpoint, $args); if (is_wp_error($response)) { return null; } $response_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); if ($response_code !== 200) { return null; } $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { return null; } if (isset($data['choices'][0]['message']['content'])) { return trim($data['choices'][0]['message']['content']); } return null; } private function parse_ai_response($ai_response, $available_routes) { if (empty($ai_response)) { return null; } // Clean up the AI response $ai_response = trim($ai_response, " \t\n\r\0\x0B\"'"); // First try exact match foreach ($available_routes as $route) { if ($ai_response === $route) { return $route; } } // Then try if AI response is contained in any route (but avoid matching "/" with everything) foreach ($available_routes as $route) { if ($route !== '/' && strpos($route, $ai_response) !== false) { return $route; } } // Then try if any route is contained in AI response (but avoid matching "/" with everything) foreach ($available_routes as $route) { if ($route !== '/' && strpos($ai_response, $route) !== false) { return $route; } } // Finally, check if AI response matches homepage specifically if ($ai_response === '/' || $ai_response === 'home' || $ai_response === 'homepage') { return '/'; } return null; } public function disable_wp_redirect_guess($redirect_url, $requested_url) { // Disable WordPress's redirect guessing when on 404 pages if (is_404()) { return false; } return $redirect_url; } private function get_cached_404_redirect($url) { global $wpdb; $table_name = $wpdb->prefix . 'ai_404_redirect_hits'; $cached = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching "SELECT redirected_to FROM `{$table_name}` WHERE url = %s AND redirected_to IS NOT NULL AND redirected_to != ''", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $url )); return $cached; } private function verify_redirect_target($target_path) { $target_url = home_url($target_path); $response = wp_remote_head($target_url, array( 'timeout' => 5, 'redirection' => 0 )); if (is_wp_error($response)) { return false; } $status_code = wp_remote_retrieve_response_code($response); return ($status_code !== 404 && $status_code !== 500); } private function clear_404_redirect_cache($url) { global $wpdb; $table_name = $wpdb->prefix . 'ai_404_redirect_hits'; $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $table_name, array('redirected_to' => null), array('url' => $url), array('%s'), array('%s') ); } private function get_manual_redirect($url) { global $wpdb; $table_name = $wpdb->prefix . 'ai_404_manual_redirects'; $redirect = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching "SELECT * FROM `{$table_name}` WHERE from_url = %s AND is_active = 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $url ), ARRAY_A); return $redirect; } private function get_full_redirect_url($to_url) { // If it starts with http/https, it's already a full URL if (strpos($to_url, 'http://') === 0 || strpos($to_url, 'https://') === 0) { return $to_url; } // If it starts with /, it's a relative path - convert to full URL if (strpos($to_url, '/') === 0) { return home_url($to_url); } // If it doesn't start with /, assume it's a relative path and add / return home_url('/' . $to_url); } private function increment_manual_redirect_count($redirect_id) { global $wpdb; $table_name = $wpdb->prefix . 'ai_404_manual_redirects'; $wpdb->query($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching "UPDATE `{$table_name}` SET hit_count = hit_count + 1 WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $redirect_id )); } private function track_404_hit($url) { global $wpdb; $table_name = $wpdb->prefix . 'ai_404_redirect_hits'; $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; $referer = isset($_SERVER['HTTP_REFERER']) ? esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER'])) : ''; $url = sanitize_text_field($url); $existing = $wpdb->get_row($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching "SELECT * FROM `{$table_name}` WHERE url = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $url )); if ($existing) { $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $table_name, array( 'hit_count' => absint($existing->hit_count) + 1, 'last_hit' => current_time('mysql') ), array('url' => $url), array('%d', '%s'), array('%s') ); } else { $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $table_name, array( 'url' => $url, 'user_agent' => $user_agent, 'referer' => $referer ), array('%s', '%s', '%s') ); } } private function update_404_redirect($url, $redirected_to) { global $wpdb; $table_name = $wpdb->prefix . 'ai_404_redirect_hits'; $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $table_name, array('redirected_to' => $redirected_to), array('url' => $url), array('%s'), array('%s') ); } public function add_admin_menu() { // Add main menu page add_menu_page( 'AI 404 Redirect', 'AI 404 Redirect', 'manage_options', 'ai-404-redirect', array($this, 'hits_page'), 'dashicons-redo', 30 ); // Add submenu pages add_submenu_page( 'ai-404-redirect', '404 Hits History', '404 Hits', 'manage_options', 'ai-404-redirect', array($this, 'hits_page') ); add_submenu_page( 'ai-404-redirect', 'Manual Redirects', 'Manual Redirects', 'manage_options', 'ai-404-redirect-manual', array($this, 'manual_redirects_page') ); } public function hits_page() { // Check user permissions if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'ai-404-redirect')); } if (isset($_POST['clear_hits']) && check_admin_referer('clear_404_hits', 'clear_hits_nonce')) { $this->clear_404_hits(); echo '
' . esc_html__('404 hits cleared successfully!', 'ai-404-redirect') . '
' . esc_html__('Redirect chains have been resolved and fixed!', 'ai-404-redirect') . '
|
→ | View |
' . sprintf(esc_html__('Error: A redirect for "%s" already exists. Please delete the existing one first or use a different URL.', 'ai-404-redirect'), esc_html($from_url)) . '
' . esc_html($success_msg) . '
' . sprintf(esc_html__('Database error: %s', 'ai-404-redirect'), esc_html($wpdb->last_error)) . '
' . esc_html__('Error: Both From URL and To URL are required.', 'ai-404-redirect') . '
' . esc_html__('Redirect deleted successfully!', 'ai-404-redirect') . '
' . esc_html__('Redirect status updated!', 'ai-404-redirect') . '
⚠️ AI 404 Redirect Notice: The following redirect/SEO plugins are active: ' . esc_html(implode(', ', $conflicting_plugins)) . '
'; echo 'These plugins may interfere with AI-powered 404 redirects. Consider disabling their 404 redirect features for best results.
'; echo '