'GPTBot (OpenAI)', 'ClaudeBot' => 'ClaudeBot (Anthropic)', 'PerplexityBot' => 'PerplexityBot', 'Google-Extended' => 'Google-Extended', 'CCBot' => 'Common Crawl (CCBot)', 'anthropic-ai' => 'Anthropic AI', 'ChatGPT-User' => 'ChatGPT-User' ]; /** * Valid access modes * * @var array */ private $valid_modes = ['allow_all', 'block_all', 'custom']; /** * Tab component instance * * @var AI_Ready_Agent_Gate_Tab */ private $tab_component; /** * Constructor */ public function __construct() { // Load dependencies $this->load_dependencies(); // Initialize hooks $this->init_hooks(); // Store original robots.txt if not already stored $this->maybe_store_original_robots_txt(); // Initialize tab component $this->tab_component = new AI_Ready_Agent_Gate_Tab($this); } /** * Load module dependencies */ private function load_dependencies() { require_once plugin_dir_path(__FILE__) . 'class-ai-ready-agent-gate-tab.php'; } /** * Initialize WordPress hooks */ private function init_hooks() { // Hook into robots.txt for filtering add_filter('robots_txt', [$this, 'filter_robots_txt'], 10, 2); } /** * Get available AI bots * * @return array Available bots array */ public function get_available_bots() { return $this->available_bots; } /** * Get valid access modes * * @return array Valid modes array */ public function get_valid_modes() { return $this->valid_modes; } /** * Get current Agent Gate mode * * @return string Current mode */ public function get_mode() { return get_option('ai_ready_agent_gate_mode', 'allow_all'); } /** * Set Agent Gate mode * * @param string $mode Mode to set * @return bool Success status */ public function set_mode($mode) { $sanitized_mode = $this->sanitize_mode($mode); return update_option('ai_ready_agent_gate_mode', $sanitized_mode); } /** * Get custom bot configuration * * @return array Custom bots array */ public function get_custom_bots() { return get_option('ai_ready_agent_gate_custom', []); } /** * Set custom bot configuration * * @param array $bots Custom bots array * @return bool Success status */ public function set_custom_bots($bots) { $sanitized_bots = $this->sanitize_custom_bots($bots); return update_option('ai_ready_agent_gate_custom', $sanitized_bots); } /** * Sanitize Agent Gate mode * * @param string $mode Mode to sanitize * @return string Sanitized mode */ public function sanitize_mode($mode) { $mode = sanitize_text_field($mode); return in_array($mode, $this->valid_modes) ? $mode : 'allow_all'; } /** * Sanitize custom bots array * * @param mixed $bots Bots to sanitize * @return array Sanitized bots array */ public function sanitize_custom_bots($bots) { if (!is_array($bots)) { return []; } // Only allow valid bot keys $valid_bot_keys = array_keys($this->available_bots); $sanitized = []; foreach ($bots as $bot) { $sanitized_bot = sanitize_text_field($bot); if (in_array($sanitized_bot, $valid_bot_keys)) { $sanitized[] = $sanitized_bot; } } return array_unique($sanitized); } /** * Reset Agent Gate settings to defaults and restore original robots.txt * * @return bool Success status */ public function reset_settings() { $mode_reset = update_option('ai_ready_agent_gate_mode', 'allow_all'); $custom_reset = update_option('ai_ready_agent_gate_custom', []); return $mode_reset && $custom_reset; } /** * Store original robots.txt content if not already stored * * @return bool Success status */ private function maybe_store_original_robots_txt() { // Check if we already have the original stored if (get_option('ai_ready_original_robots_txt') !== false) { return true; // Already stored } // Get the original robots.txt content $original_content = $this->get_original_robots_txt_content(); // Store it return update_option('ai_ready_original_robots_txt', $original_content); } /** * Get original robots.txt content from the site * * @return string Original robots.txt content */ private function get_original_robots_txt_content() { $is_public = get_option('blog_public'); // First try to get content via HTTP request (avoids filter conflicts) $original_content = ''; $robots_url = home_url('/robots.txt'); $response = wp_remote_get($robots_url, ['timeout' => 10]); if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { $original_content = wp_remote_retrieve_body($response); // Remove any existing AI-Ready Core rules from the content $original_content = $this->remove_agent_gate_rules($original_content); } // If HTTP request failed, try filters (but be careful about recursion) if (empty($original_content)) { // Use WordPress default robots.txt generation $original_content = apply_filters('robots_txt', '', $is_public); } // If still empty, create a basic default if (empty($original_content)) { $original_content = "User-agent: *\nDisallow:\n"; } return $original_content; } /** * Remove Agent Gate rules from robots.txt content * * @param string $content robots.txt content * @return string Cleaned content */ private function remove_agent_gate_rules($content) { // Remove our section marker and everything after it $marker_pos = strpos($content, '# AI-Ready Core: Agent Gate Rules'); if ($marker_pos !== false) { $content = substr($content, 0, $marker_pos); // Remove trailing whitespace $content = rtrim($content); } return $content; } /** * Get stored original robots.txt content * * @return string Original robots.txt content */ public function get_original_robots_txt() { return get_option('ai_ready_original_robots_txt', ''); } /** * Restore original robots.txt by removing Agent Gate modifications * This is achieved by setting mode to 'allow_all' which removes all restrictions * * @return bool Success status */ public function restore_original_robots_txt() { // Set mode to allow_all to remove all Agent Gate restrictions $mode_reset = $this->set_mode('allow_all'); $custom_reset = $this->set_custom_bots([]); return $mode_reset && $custom_reset; } /** * Filter robots.txt content for Agent Gate functionality * * @param string $output Existing robots.txt content * @param bool $public Whether site is public * @return string Modified robots.txt content */ public function filter_robots_txt($output, $public) { // Only modify if site is public if (!$public) { return $output; } $gate_mode = $this->get_mode(); $custom_bots = $this->get_custom_bots(); $available_bot_keys = array_keys($this->available_bots); $additional_rules = ''; switch ($gate_mode) { case 'block_all': // Block all AI agents foreach ($available_bot_keys as $bot) { $additional_rules .= "User-agent: {$bot}\n"; $additional_rules .= "Disallow: /\n\n"; } break; case 'custom': // Block only unselected AI agents $blocked_bots = array_diff($available_bot_keys, $custom_bots); foreach ($blocked_bots as $bot) { $additional_rules .= "User-agent: {$bot}\n"; $additional_rules .= "Disallow: /\n\n"; } break; case 'allow_all': default: // No additional restrictions for AI agents break; } // Add our rules to the existing robots.txt output if (!empty($additional_rules)) { $output .= "\n# AI-Ready Core: Agent Gate Rules\n"; $output .= $additional_rules; } return $output; } /** * Get robots.txt preview for current settings * * @param string|null $mode Mode to preview (current if null) * @param array|null $custom_bots Custom bots to preview (current if null) * @return string Formatted robots.txt preview */ public function get_robots_txt_preview($mode = null, $custom_bots = null) { if ($mode === null) { $mode = $this->get_mode(); } if ($custom_bots === null) { $custom_bots = $this->get_custom_bots(); } $preview = '
';
        
        // Ensure $custom_bots is an array and escape each value
        $custom_bots = array_map('esc_html', (array)$custom_bots);
        
        switch ($mode) {
            case 'allow_all':
                $preview .= esc_html(__('# No AI agent restrictions - all bots allowed', 'ai-ready-core'));
                break;
                
            case 'block_all':
                foreach (array_keys($this->available_bots) as $bot_key) {
                    $preview .= esc_html(sprintf("User-agent: %s\nDisallow: /\n\n", $bot_key));
                }
                break;
                
            case 'custom':
                $blocked_bots = array_diff(array_keys($this->available_bots), $custom_bots);
                if (empty($blocked_bots)) {
                    $preview .= esc_html(__('# All selected AI agents allowed - no restrictions', 'ai-ready-core'));
                } else {
                    foreach ($blocked_bots as $bot_key) {
                        $preview .= esc_html(sprintf("User-agent: %s\nDisallow: /\n\n", $bot_key));
                    }
                }
                break;
        }
        
        $preview .= '
'; return $preview; } /** * Process form submission * * @return bool Success status */ public function process_form_submission() { // Verify nonce for security if (!check_admin_referer('ai_ready_settings_nonce')) { return false; } $success = true; // Process mode setting if (isset($_POST['ai_ready_agent_gate_mode'])) { $mode = sanitize_text_field(wp_unslash($_POST['ai_ready_agent_gate_mode'])); if (!$this->set_mode($mode)) { $success = false; } } // Process custom bots setting if (isset($_POST['ai_ready_agent_gate_custom']) && is_array($_POST['ai_ready_agent_gate_custom'])) { $custom_bots = array_map('sanitize_text_field', wp_unslash($_POST['ai_ready_agent_gate_custom'])); if (!$this->set_custom_bots($custom_bots)) { $success = false; } } else { // If no custom bots selected, clear the setting if (!$this->set_custom_bots([])) { $success = false; } } return $success; } /** * Get tab component instance * * @return AI_Ready_Agent_Gate_Tab Tab component */ public function get_tab_component() { return $this->tab_component; } /** * Register WordPress settings */ public function register_settings() { // Register settings for Agent Gate register_setting( 'ai_ready_settings', 'ai_ready_agent_gate_mode', [ 'type' => 'string', 'sanitize_callback' => __CLASS__ . '::sanitize_mode_static', 'default' => 'allow_all' ] ); register_setting( 'ai_ready_settings', 'ai_ready_agent_gate_custom', [ 'type' => 'array', 'sanitize_callback' => __CLASS__ . '::sanitize_custom_bots_static', 'default' => [] ] ); } /** * Static wrapper for sanitize_mode * * @param string $mode Mode to sanitize * @return string Sanitized mode */ public static function sanitize_mode_static($mode) { $instance = new self(); return $instance->sanitize_mode($mode); } /** * Static wrapper for sanitize_custom_bots * * @param mixed $bots Bots to sanitize * @return array Sanitized bots array */ public static function sanitize_custom_bots_static($bots) { $instance = new self(); return $instance->sanitize_custom_bots($bots); } /** * Get diagnostics information for Agent Gate * * @return array Diagnostics data */ public function get_diagnostics() { $gate_mode = $this->get_mode(); $custom_bots = $this->get_custom_bots(); // Get current robots.txt content to analyze $is_public = get_option('blog_public'); $robots_content = apply_filters('robots_txt', '', $is_public); // Fallback to HTTP request if filter didn't provide content if (empty($robots_content)) { $robots_url = home_url('/robots.txt'); $response = wp_remote_get($robots_url, ['timeout' => 10]); if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) { $robots_content = wp_remote_retrieve_body($response); } } $ai_bots = array_keys($this->available_bots); $blocked_bots = []; $allowed_bots = []; $status_details = []; foreach ($ai_bots as $bot) { // Check if this bot should be blocked according to Agent Gate settings $should_be_blocked = false; switch ($gate_mode) { case 'block_all': $should_be_blocked = true; break; case 'custom': $should_be_blocked = !in_array($bot, $custom_bots); break; case 'allow_all': default: $should_be_blocked = false; break; } // Check actual robots.txt content $is_blocked_in_robots = preg_match('/User-agent:\s*' . preg_quote($bot) . '\s*\n\s*Disallow:\s*\//i', $robots_content); if ($is_blocked_in_robots) { $blocked_bots[] = $bot; $status_details[] = "❌ {$bot}: Blocked"; } else { if ($should_be_blocked) { // Bot should be blocked but isn't - this is a problem $status_details[] = "⚠️ {$bot}: Should be blocked but isn't"; } else { $status_details[] = "✅ {$bot}: Allowed (no restrictions)"; } } } $check = [ 'name' => __('AI Bot Access Control (Agent Gate)', 'ai-ready-core'), 'status' => 'pass', 'message' => '', 'recommendation' => '', 'category' => 'configuration' ]; // Determine overall status and message based on Agent Gate configuration switch ($gate_mode) { case 'allow_all': if (empty($blocked_bots)) { $check['status'] = 'pass'; $check['message'] = __('🟢 Agent Gate: All AI bots are allowed', 'ai-ready-core'); $check['recommendation'] = __('Excellent! All AI agents can access your content for maximum discoverability.', 'ai-ready-core'); } else { $check['status'] = 'warning'; /* translators: %s: Comma-separated list of bot names that are blocked */ $check['message'] = sprintf(__('⚠️ Agent Gate is set to "Allow All" but some bots are blocked: %s', 'ai-ready-core'), implode(', ', $blocked_bots)); $check['recommendation'] = __('Review your robots.txt configuration to ensure it matches your intended access policy.', 'ai-ready-core'); } break; case 'block_all': $unblocked_bots = array_diff($ai_bots, $blocked_bots); if (empty($unblocked_bots)) { $check['status'] = 'info'; $check['message'] = __('🔒 Agent Gate: All AI bots are blocked', 'ai-ready-core'); $check['recommendation'] = __('You are blocking all AI bots as configured. This protects privacy but prevents AI discovery. Use the Agent Gate tab to allow specific bots if desired.', 'ai-ready-core'); } else { $check['status'] = 'warning'; /* translators: %s: Comma-separated list of bot names that are not blocked */ $check['message'] = sprintf(__('⚠️ Agent Gate is set to "Block All" but some bots are not blocked: %s', 'ai-ready-core'), implode(', ', $unblocked_bots)); $check['recommendation'] = __('The robots.txt file is not reflecting your Agent Gate settings. This may be due to caching or another plugin modifying robots.txt. Try clearing any caches or check for conflicting plugins.', 'ai-ready-core'); } break; case 'custom': $allowed_count = count($custom_bots); $blocked_count = count($ai_bots) - $allowed_count; /* translators: 1: Number of allowed bots, 2: Number of blocked bots */ $check['message'] = sprintf(__('⚙️ Agent Gate: Custom configuration - %1$d allowed, %2$d blocked', 'ai-ready-core'), $allowed_count, $blocked_count); /* translators: 1: Number of allowed bots, 2: Number of blocked bots */ $check['recommendation'] = sprintf(__('You have selectively configured AI bot access. %1$d bots are allowed and %2$d are blocked. Adjust settings in the Agent Gate tab if needed.', 'ai-ready-core'), $allowed_count, $blocked_count); break; default: $check['status'] = 'warning'; $check['message'] = __('⚠️ Agent Gate: Unknown configuration', 'ai-ready-core'); $check['recommendation'] = __('Agent Gate configuration appears to be invalid. Please check your settings in the Agent Gate tab.', 'ai-ready-core'); break; } // Add detailed bot status if (!empty($status_details)) { $check['message'] .= "\n\nBot Status:\n" . implode("\n", $status_details); } return $check; } /** * Initialize Agent Gate on plugin activation * Store original robots.txt content */ public static function on_plugin_activation() { // Only store if we don't already have it if (get_option('ai_ready_original_robots_txt') === false) { // Get WordPress default robots.txt (without our modifications) $is_public = get_option('blog_public'); $original_content = apply_filters('robots_txt', '', $is_public); // If no content from filters, create a basic default if (empty($original_content)) { $original_content = "User-agent: *\nDisallow:\n"; } // Store the original content update_option('ai_ready_original_robots_txt', $original_content); } } /** * Cleanup Agent Gate settings on plugin uninstall */ public static function cleanup_settings() { delete_option('ai_ready_agent_gate_mode'); delete_option('ai_ready_agent_gate_custom'); delete_option('ai_ready_original_robots_txt'); } }