get_charset_collate(); // Create the agent_ai_bot_protect_counts table for aggregated data $counts_table = $wpdb->prefix . 'agent_ai_bot_protect_counts'; $counts_sql = "CREATE TABLE IF NOT EXISTS $counts_table ( id bigint(20) NOT NULL AUTO_INCREMENT, bot_name varchar(255) NOT NULL, date_recorded date NOT NULL, hit_count int NOT NULL DEFAULT 1, blocked_count int NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY bot_date (bot_name, date_recorded) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($counts_sql); // Set version in options update_option('agent_ai_bot_protect_db_version', '1.2'); } register_activation_hook(__FILE__, 'agent_ai_bot_protect_activate'); // Check and update database structure function agent_ai_bot_protect_check_db_updates() { $current_version = get_option('agent_ai_bot_protect_db_version', '1.0'); // If we're already at the latest version, no need to continue if (version_compare($current_version, '1.2', '>=')) { return; } global $wpdb; // Update from 1.0 to 1.1 (add is_blocked column) if (version_compare($current_version, '1.1', '<')) { $table_name = $wpdb->prefix . 'agent_ai_bot_protect_logs'; // Check if the table exists // Direct DB query is necessary here for schema operations that WordPress doesn't provide abstracted functions for // Using caching to avoid repeated queries $cache_key = 'agent_ai_bot_protect_table_exists_' . $table_name; $table_exists = wp_cache_get($cache_key); if (false === $table_exists) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Schema check requires direct query $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) === $table_name; wp_cache_set($cache_key, $table_exists, '', 3600); // Cache for 1 hour } if ($table_exists) { // Check if is_blocked column exists // Direct DB query is necessary for schema inspection $column_cache_key = 'agent_ai_bot_protect_column_exists_is_blocked_' . $table_name; $column_exists = wp_cache_get($column_cache_key); if (false === $column_exists) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Schema check requires direct query $column_exists = $wpdb->get_results( $wpdb->prepare( "SHOW COLUMNS FROM {$wpdb->prefix}agent_ai_bot_protect_logs LIKE %s", 'is_blocked' ) ); wp_cache_set($column_cache_key, $column_exists, '', 3600); // Cache for 1 hour } if (empty($column_exists)) { // Add the is_blocked column using dbDelta which is WordPress' recommended way to handle schema changes require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); // Create a temporary table definition that includes the new column $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS {$table_name} ( id bigint(20) NOT NULL AUTO_INCREMENT, user_agent text NOT NULL, accessed_url varchar(255) NOT NULL, timestamp datetime NOT NULL, is_blocked tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (id) ) $charset_collate;"; // Use dbDelta to add the column dbDelta($sql); // Invalidate the cache since we modified the schema wp_cache_delete($column_cache_key); } } } // Update from 1.1 to 1.2 (add counts table) if (version_compare($current_version, '1.2', '<')) { $counts_table = $wpdb->prefix . 'agent_ai_bot_protect_counts'; $charset_collate = $wpdb->get_charset_collate(); $counts_sql = "CREATE TABLE IF NOT EXISTS $counts_table ( id bigint(20) NOT NULL AUTO_INCREMENT, bot_name varchar(255) NOT NULL, date_recorded date NOT NULL, hit_count int NOT NULL DEFAULT 1, blocked_count int NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY bot_date (bot_name, date_recorded) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($counts_sql); } // Update version update_option('agent_ai_bot_protect_db_version', '1.2'); } add_action('plugins_loaded', 'agent_ai_bot_protect_check_db_updates'); // Extract disallowed bots from robots.txt function agent_ai_bot_protect_get_disallowed_bots() { $robots_txt_content = get_option('agent_ai_bot_protect_robots_txt', ''); $disallowed_bots = array(); // Extract bot names from User-agent lines if (!empty($robots_txt_content)) { $lines = explode("\n", $robots_txt_content); $current_bot = null; foreach ($lines as $line) { $line = trim($line); // Skip comments and empty lines if (empty($line) || strpos($line, '#') === 0) { continue; } // Check for User-agent lines if (preg_match('/^User-agent:\s*(.+)$/i', $line, $matches)) { $current_bot = trim($matches[1]); // Skip the wildcard user agent if ($current_bot !== '*') { $disallowed_bots[] = $current_bot; } } } } return $disallowed_bots; } // Check and block disallowed bots function agent_ai_bot_protect_check_and_block() { // Skip for admin users if (is_admin() || current_user_can('manage_options')) { return; } // Check if blocking is enabled $blocking_enabled = get_option('agent_ai_bot_protect_blocking_enabled', '1') === '1'; // Get user agent $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; if (empty($user_agent)) { return; } // Get bots from plugin's robots.txt file (for tracking only) $plugin_robots_txt_path = plugin_dir_path(__FILE__) . 'dist/assets/robots.txt'; $known_bots = array(); if (file_exists($plugin_robots_txt_path)) { $robots_content = file_get_contents($plugin_robots_txt_path); $lines = explode("\n", $robots_content); foreach ($lines as $line) { $line = trim($line); // Skip empty lines and disallow directives if (empty($line) || strpos($line, 'Disallow:') === 0) { continue; } // Extract bot name from User-agent line if (preg_match('/^User-agent:\s*(.+)$/i', $line, $matches)) { $bot_name = trim($matches[1]); // Skip the wildcard user agent if ($bot_name !== '*') { $known_bots[] = $bot_name; } } } } // Get disallowed bots from site's robots.txt (via WordPress option) - these will be blocked $disallowed_bots = agent_ai_bot_protect_get_disallowed_bots(); // Flag to track if we've already counted this bot $bot_counted = false; $matched_bot_name = ''; // First check if the user agent matches any disallowed bot from the site's robots.txt (for blocking) if ($blocking_enabled) { foreach ($disallowed_bots as $bot) { if (stripos($user_agent, $bot) !== false) { // Log the bot hit and mark as blocked agent_ai_bot_protect_increment_bot_count($bot, true); $bot_counted = true; $matched_bot_name = $bot; // Send 403 Forbidden response status_header(403); nocache_headers(); echo '' . esc_html__('403 Forbidden', 'agent-ai-bot-protect') . ''; echo '

' . esc_html__('403 Forbidden', 'agent-ai-bot-protect') . '

'; echo '

' . esc_html__('Access to this resource is denied by Agent AI Bot Protect.', 'agent-ai-bot-protect') . '

'; echo ''; exit; } } } // If not already counted as a disallowed bot, check if it matches any known bot from the plugin's list if (!$bot_counted) { foreach ($known_bots as $bot) { if (stripos($user_agent, $bot) !== false) { // Log the bot hit (increment counter) but don't block agent_ai_bot_protect_increment_bot_count($bot, false); break; // Only count once if multiple patterns match } } } } // Increment bot count function agent_ai_bot_protect_increment_bot_count($bot_name, $was_blocked = false) { global $wpdb; $table_name = $wpdb->prefix . 'agent_ai_bot_protect_counts'; $today = current_time('Y-m-d'); // Direct DB queries are necessary here for performance reasons when tracking bot hits // We're using a custom table structure optimized for aggregating bot statistics // Using a transient-based locking mechanism to prevent race conditions on high-traffic sites $lock_key = 'agent_ai_bot_protect_lock_' . md5($bot_name . $today); // Try to get the lock if (get_transient($lock_key)) { // Another process is updating this record, wait a moment and try again usleep(100000); // Wait 100ms agent_ai_bot_protect_increment_bot_count($bot_name, $was_blocked); return; } // Set a short lock to prevent race conditions set_transient($lock_key, true, 5); // 5 second lock try { // Cache key for this specific bot and date $cache_key = 'agent_ai_bot_protect_bot_' . md5($bot_name . $today); $record_exists = wp_cache_get($cache_key); // Try to update existing record for today // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for atomic counter increment with proper caching implemented $result = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}agent_ai_bot_protect_counts SET hit_count = hit_count + 1, blocked_count = blocked_count + %d WHERE bot_name = %s AND date_recorded = %s", $was_blocked ? 1 : 0, $bot_name, $today ) ); // If no record was updated, insert a new one if ($result === 0) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Insert operation requires direct query $wpdb->insert( $table_name, [ 'bot_name' => $bot_name, 'date_recorded' => $today, 'hit_count' => 1, 'blocked_count' => $was_blocked ? 1 : 0 ], ['%s', '%s', '%d', '%d'] ); // Set cache to indicate this record now exists wp_cache_set($cache_key, true, '', 3600); // Cache for 1 hour } } finally { // Always release the lock delete_transient($lock_key); } } // Hook into WordPress init to check and block bots add_action('init', 'agent_ai_bot_protect_check_and_block', 1); // Enqueue Angular scripts and styles function agent_ai_bot_protect_enqueue_scripts() { $plugin_dir_url = plugin_dir_url(__FILE__); // Check if we're NOT in admin if (!is_admin()) { // Register and enqueue main analytics script wp_register_script( 'tracking-agent', $plugin_dir_url . 'tracking-agent.js', array(), '1.0.0', true ); // Add initial data capture script wp_add_inline_script( 'tracking-agent', 'window._botShieldInitialData = { userAgent: navigator.userAgent, timestamp: new Date().getTime(), referrer: document.referrer // Add any other critical initial data you need };', 'before' ); // Enqueue the script wp_enqueue_script('tracking-agent'); // Add async attribute to the script add_filter('script_loader_tag', function($tag, $handle) { if ('tracking-agent' === $handle) { return str_replace(' src', ' async src', $tag); } return $tag; }, 10, 2); } // Only load admin-specific scripts on our admin page // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No action is taken here, just checking page for script loading if (is_admin() && isset($_GET['page']) && 'agent-ai-bot-protect' === sanitize_key(wp_unslash($_GET['page']))) { // This is only used to determine which script to load on admin pages // No sensitive operation is performed based on this value, just script enqueueing // Additionally, it's protected by the 'manage_options' capability requirement // Enqueue the module script wp_enqueue_script( 'agent-ai-bot-protect-module', $plugin_dir_url . 'dist/agent-ai-bot-protect.js', array(), '1.0.0', true ); // Add the module type to the script tag add_filter('script_loader_tag', function($tag, $handle) { if ('agent-ai-bot-protect-module' === $handle) { return str_replace(' src', ' type="module" src', $tag); } return $tag; }, 10, 2); // Add the REST API nonce to the script wp_localize_script( 'agent-ai-bot-protect-module', 'AgentAIBotProtectData', array( 'wpRestNonce' => wp_create_nonce('wp_rest') ) ); // Enqueue the styles normally wp_enqueue_style( 'agent-ai-bot-protect-styles', $plugin_dir_url . 'dist/agent-ai-bot-protect.css', [], '1.0.0' ); } } // Hook into both admin and frontend script enqueueing add_action('admin_enqueue_scripts', 'agent_ai_bot_protect_enqueue_scripts'); add_action('wp_enqueue_scripts', 'agent_ai_bot_protect_enqueue_scripts'); // Add settings link to plugins page function agent_ai_bot_protect_add_settings_link($links) { $settings_link = 'Settings'; array_unshift($links, $settings_link); return $links; } add_filter('plugin_action_links_agent-ai-bot-protect/agent-ai-bot-protect.php', 'agent_ai_bot_protect_add_settings_link'); // Add menu item function agent_ai_bot_protect_add_admin_menu() { add_menu_page( 'Agent AI Bot Protect', // Page title 'Agent AI Bot Protect', // Menu title 'manage_options', // Capability 'agent-ai-bot-protect', // Menu slug function() { // Callback function // This is an admin page that only users with manage_options capability can access // We're just checking if page=agent-ai-bot-protect in the URL to determine which admin page to load // No form submission is being processed here, just loading the Angular app ?>
'POST', 'callback' => 'agent_ai_bot_protect_save_robots_txt', 'permission_callback' => function() { return current_user_can('manage_options'); } ]); // New endpoint for log analysis register_rest_route('agent-ai-bot-protect/v1', '/analyze-logs', [ 'methods' => 'GET', 'callback' => 'agent_ai_bot_protect_analyze_logs', 'permission_callback' => function() { return current_user_can('manage_options'); } ]); // Add endpoint for getting bot stats by date range register_rest_route('agent-ai-bot-protect/v1', '/bot-stats', [ 'methods' => 'GET', 'callback' => 'agent_ai_bot_protect_get_stats', 'permission_callback' => function() { return current_user_can('manage_options'); }, 'args' => [ 'start_date' => [ 'required' => false, 'type' => 'string' ], 'end_date' => [ 'required' => false, 'type' => 'string' ] ] ]); // Add this to your agent_ai_bot_protect_register_routes function register_rest_route('agent-ai-bot-protect/v1', '/log-visit', [ 'methods' => 'POST', 'callback' => 'agent_ai_bot_protect_log_visit', 'permission_callback' => '__return_true', // Allow public access 'args' => [ 'userAgent' => [ 'required' => true, 'type' => 'string' ], 'timestamp' => [ 'required' => true ], 'referrer' => [ 'required' => false, 'type' => 'string' ], 'url' => [ 'required' => true, 'type' => 'string' ] ] ]); // Add endpoint to toggle bot blocking register_rest_route('agent-ai-bot-protect/v1', '/toggle-blocking', [ 'methods' => 'POST', 'callback' => 'agent_ai_bot_protect_toggle_blocking', 'permission_callback' => function() { return current_user_can('manage_options'); }, 'args' => [ 'enabled' => [ 'required' => true, 'type' => 'boolean' ] ] ]); // Add endpoint to get blocking status register_rest_route('agent-ai-bot-protect/v1', '/blocking-status', [ 'methods' => 'GET', 'callback' => 'agent_ai_bot_protect_get_blocking_status', 'permission_callback' => function() { return current_user_can('manage_options'); } ]); // Add endpoint for robots.txt URL management register_rest_route('agent-ai-bot-protect/v1', '/robots-txt-url', [ 'methods' => 'GET', 'callback' => 'agent_ai_bot_protect_get_robots_txt_url', 'permission_callback' => function() { return current_user_can('manage_options'); } ]); register_rest_route('agent-ai-bot-protect/v1', '/robots-txt-url', [ 'methods' => 'POST', 'callback' => 'agent_ai_bot_protect_save_robots_txt_url', 'permission_callback' => function() { return current_user_can('manage_options'); }, 'args' => [ 'url' => [ 'required' => true, 'type' => 'string' ] ] ]); } add_action('rest_api_init', 'agent_ai_bot_protect_register_routes'); // Handle saving robots.txt file function agent_ai_bot_protect_save_robots_txt($request) { // Get content parameter and ensure it's a string $content = $request->get_param('content'); $content = is_null($content) ? '' : sanitize_textarea_field((string)$content); // Check if we should clear the BotShield section $clear = $request->get_param('clear'); $should_clear = !empty($clear); // Debug logging // Read existing content $existing_content = get_option('agent_ai_bot_protect_robots_txt', ''); if (empty($existing_content)) { // Default robots.txt content if none exists $existing_content = "User-agent: *\nDisallow: /wp-admin/\nAllow: /wp-admin/admin-ajax.php\n"; } // Remove old BotShield section if it exists $pattern = '/# Begin BotShield.*# End BotShield\n*/s'; $existing_content = preg_replace($pattern, '', $existing_content); // If content is empty or clear is true, just save without BotShield section if (empty($content) || $should_clear) { $final_content = trim($existing_content) . "\n"; } else { // Prepare new BotShield section $new_section = "# Begin BotShield\n"; $new_section .= $content . "\n"; $new_section .= "# End BotShield\n"; // Combine content $final_content = trim($existing_content . "\n" . $new_section) . "\n"; } // Save the final content to WordPress option update_option('agent_ai_bot_protect_robots_txt', $final_content); // Return the final content return [ 'success' => true, 'message' => 'robots.txt file updated successfully', 'final_content' => $final_content ]; } // Add this function to serve the robots.txt content function agent_ai_bot_protect_serve_robots_txt($robots_txt, $public = 1) { $custom_content = get_option('agent_ai_bot_protect_robots_txt'); if (!empty($custom_content)) { return $custom_content; } return $robots_txt; } add_filter('robots_txt', 'agent_ai_bot_protect_serve_robots_txt'); // Analyze logs function agent_ai_bot_protect_analyze_logs() { global $wpdb; $counts_table = $wpdb->prefix . 'agent_ai_bot_protect_counts'; // Direct DB queries are necessary here for complex aggregation queries on custom tables // We're implementing caching to improve performance // Cache key for the entire analytics response $cache_key = 'agent_ai_bot_protect_analytics_30days'; $cached_response = wp_cache_get($cache_key); if (false !== $cached_response) { return rest_ensure_response(maybe_unserialize($cached_response)); } // Get total hits and blocks for the last 30 days $stats_cache_key = 'agent_ai_bot_protect_stats_totals_30days'; $stats = wp_cache_get($stats_cache_key); if (false === $stats) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Analytics query with proper caching $stats = $wpdb->get_row( "SELECT SUM(hit_count) as total_hits, SUM(blocked_count) as total_blocks FROM {$wpdb->prefix}agent_ai_bot_protect_counts WHERE date_recorded >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)" ); wp_cache_set($stats_cache_key, $stats, '', 3600); // Cache for 1 hour } $total_hits = $stats ? (int)$stats->total_hits : 0; $total_blocks = $stats ? (int)$stats->total_blocks : 0; // Get bot-specific stats $bot_stats_cache_key = 'agent_ai_bot_protect_bot_stats_30days'; $bot_stats = wp_cache_get($bot_stats_cache_key); if (false === $bot_stats) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Analytics query with proper caching $bot_stats = $wpdb->get_results( "SELECT bot_name, SUM(hit_count) as total, SUM(blocked_count) as blocked FROM {$wpdb->prefix}agent_ai_bot_protect_counts WHERE date_recorded >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY bot_name ORDER BY total DESC" ); wp_cache_set($bot_stats_cache_key, $bot_stats, '', 3600); // Cache for 1 hour } // Get daily stats for the last 30 days $daily_stats_cache_key = 'agent_ai_bot_protect_daily_stats_30days'; $daily_stats = wp_cache_get($daily_stats_cache_key); if (false === $daily_stats) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Analytics query with proper caching $daily_stats = $wpdb->get_results( "SELECT date_recorded, SUM(hit_count) as hits, SUM(blocked_count) as blocks FROM {$wpdb->prefix}agent_ai_bot_protect_counts WHERE date_recorded >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY date_recorded ORDER BY date_recorded DESC" ); wp_cache_set($daily_stats_cache_key, $daily_stats, '', 3600); // Cache for 1 hour } $response = array( 'success' => true, 'data' => array( 'total_requests' => $total_hits, 'blocked_requests' => $total_blocks, 'detected_bots' => array_reduce($bot_stats, function($carry, $item) { $carry[$item->bot_name] = array( 'total' => (int)$item->total, 'blocked' => (int)$item->blocked ); return $carry; }, array()), 'daily_stats' => array_map(function($day) { return array( 'date' => $day->date_recorded, 'hits' => (int)$day->hits, 'blocks' => (int)$day->blocks ); }, $daily_stats) ) ); // Cache the entire response wp_cache_set($cache_key, maybe_serialize($response), '', 3600); // Cache for 1 hour return rest_ensure_response($response); } // Log visit (for client-side detection - keep for backward compatibility) function agent_ai_bot_protect_log_visit($request) { $data = $request->get_params(); // Sanitize the data $userAgent = isset($data['userAgent']) ? sanitize_text_field($data['userAgent']) : ''; $is_bot = preg_match('/bot|crawl|spider|slurp|search|agent/i', $userAgent) ? 1 : 0; if ($is_bot) { $bot_name = agent_ai_bot_protect_extract_bot_name($userAgent); // Only increment count for bots agent_ai_bot_protect_increment_bot_count($bot_name, false); } return rest_ensure_response([ 'success' => true, 'is_bot' => $is_bot ]); } // Extract bot name function agent_ai_bot_protect_extract_bot_name($user_agent) { if (preg_match('/(?:bot|crawl|slurp|spider|search|agent)(?:\s*|\/)([^\s;)]*)/i', $user_agent, $matches)) { return $matches[1] ?: 'Unknown Bot'; } return 'Unknown Bot'; } // Add CORS headers function agent_ai_bot_protect_add_cors_headers() { header('Access-Control-Allow-Origin: ' . esc_url_raw(site_url())); header('Access-Control-Allow-Methods: POST, GET, OPTIONS'); header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept'); // Properly validate and unslash the REQUEST_METHOD if (isset($_SERVER['REQUEST_METHOD']) && sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) === 'OPTIONS') { status_header(200); exit(); } } add_action('rest_api_init', function() { remove_filter('rest_pre_serve_request', 'rest_send_cors_headers'); add_filter('rest_pre_serve_request', 'agent_ai_bot_protect_add_cors_headers'); }, 15); function agent_ai_bot_protect_admin_styles() { // Register an empty stylesheet for our admin styles wp_register_style( 'agent-ai-bot-protect-admin-styles', false, array(), // No dependencies '1.0.0' // Version number ); // Add our inline styles wp_add_inline_style( 'agent-ai-bot-protect-admin-styles', ' #toplevel_page_agent-ai-bot-protect .wp-menu-image img { height: 35px; padding: 0px; margin-top: 5px; margin-left: 2px; } ' ); // Enqueue the style wp_enqueue_style('agent-ai-bot-protect-admin-styles'); } add_action('admin_head', 'agent_ai_bot_protect_admin_styles'); // Toggle bot blocking function agent_ai_bot_protect_toggle_blocking($request) { $enabled = (bool)$request->get_param('enabled'); update_option('agent_ai_bot_protect_blocking_enabled', $enabled ? '1' : '0'); return rest_ensure_response([ 'success' => true, 'enabled' => $enabled ]); } // Get blocking status function agent_ai_bot_protect_get_blocking_status() { $enabled = get_option('agent_ai_bot_protect_blocking_enabled', '1'); return rest_ensure_response([ 'success' => true, 'enabled' => $enabled === '1' ]); } // Get bot stats by date range function agent_ai_bot_protect_get_stats($request) { global $wpdb; $counts_table = $wpdb->prefix . 'agent_ai_bot_protect_counts'; // Get date range parameters with validation $start_date = $request->get_param('start_date'); $end_date = $request->get_param('end_date'); // Log initial date parameters // Validate and sanitize dates if (!empty($start_date)) { $start_date = sanitize_text_field($start_date); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $start_date)) { $start_date = gmdate('Y-m-d', strtotime('-30 days')); } } else { $start_date = gmdate('Y-m-d', strtotime('-30 days')); } if (!empty($end_date)) { $end_date = sanitize_text_field($end_date); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $end_date)) { $end_date = gmdate('Y-m-d'); } } else { $end_date = gmdate('Y-m-d'); } // Cache key for the entire stats response for this date range $cache_key = 'agent_ai_bot_protect_stats_' . md5($start_date . '_' . $end_date); $cached_response = wp_cache_get($cache_key); if (false !== $cached_response) { return rest_ensure_response(maybe_unserialize($cached_response)); } // Get bot stats for the date range $bot_stats_cache_key = 'agent_ai_bot_protect_bot_stats_' . md5($start_date . '_' . $end_date); $bot_stats = wp_cache_get($bot_stats_cache_key); if (false === $bot_stats) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Analytics query with proper caching $bot_stats = $wpdb->get_results( $wpdb->prepare( "SELECT bot_name, SUM(hit_count) as total, SUM(blocked_count) as blocked FROM {$wpdb->prefix}agent_ai_bot_protect_counts WHERE date_recorded BETWEEN %s AND %s GROUP BY bot_name ORDER BY total DESC", $start_date, $end_date ) ); wp_cache_set($bot_stats_cache_key, $bot_stats, '', 3600); // Cache for 1 hour } // Get daily stats for the date range $daily_stats_cache_key = 'agent_ai_bot_protect_daily_stats_' . md5($start_date . '_' . $end_date); $daily_stats = wp_cache_get($daily_stats_cache_key); if (false === $daily_stats) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Analytics query with proper caching $daily_stats = $wpdb->get_results( $wpdb->prepare( "SELECT date_recorded, SUM(hit_count) as hits, SUM(blocked_count) as blocks FROM {$wpdb->prefix}agent_ai_bot_protect_counts WHERE date_recorded BETWEEN %s AND %s GROUP BY date_recorded ORDER BY date_recorded ASC", $start_date, $end_date ) ); wp_cache_set($daily_stats_cache_key, $daily_stats, '', 3600); // Cache for 1 hour } // Ensure we have arrays even if no data $bot_stats = is_array($bot_stats) ? $bot_stats : array(); $daily_stats = is_array($daily_stats) ? $daily_stats : array(); // Process bot stats with type casting and validation $processed_bots = array(); foreach ($bot_stats as $stat) { if (isset($stat->bot_name) && is_string($stat->bot_name)) { $processed_bots[$stat->bot_name] = array( 'total' => (int)($stat->total ?? 0), 'blocked' => (int)($stat->blocked ?? 0) ); } } // Process daily stats with type casting and validation $processed_daily = array(); foreach ($daily_stats as $stat) { if (isset($stat->date_recorded) && is_string($stat->date_recorded)) { $processed_daily[] = array( 'date' => $stat->date_recorded, 'hits' => (int)($stat->hits ?? 0), 'blocks' => (int)($stat->blocks ?? 0) ); } } $response = array( 'success' => true, 'data' => array( 'start_date' => $start_date, 'end_date' => $end_date, 'bots' => $processed_bots, 'daily' => $processed_daily ) ); // Log the final response before caching // Cache the entire response wp_cache_set($cache_key, maybe_serialize($response), '', 3600); // Cache for 1 hour return rest_ensure_response($response); } // Get robots.txt URL function agent_ai_bot_protect_get_robots_txt_url() { $url = get_option('agent_ai_bot_protect_robots_txt_url', ''); return rest_ensure_response(['url' => $url]); } // Save robots.txt URL function agent_ai_bot_protect_save_robots_txt_url($request) { $url = sanitize_url($request->get_param('url')); if (empty($url)) { return new WP_Error('invalid_url', 'URL cannot be empty', ['status' => 400]); } update_option('agent_ai_bot_protect_robots_txt_url', $url); return rest_ensure_response(['success' => true]); }