> */ public static function bots(): array { $bots = array( // ---- United States ------------------------------------------ 'gptbot' => array( 'ua' => 'GPTBot', 'vendor' => 'OpenAI', 'purpose' => 'training', 'region' => 'us' ), 'oai-searchbot' => array( 'ua' => 'OAI-SearchBot', 'vendor' => 'OpenAI', 'purpose' => 'search', 'region' => 'us' ), 'chatgpt-user' => array( 'ua' => 'ChatGPT-User', 'vendor' => 'OpenAI', 'purpose' => 'on-demand', 'region' => 'us' ), 'claudebot' => array( 'ua' => 'ClaudeBot', 'vendor' => 'Anthropic', 'purpose' => 'training', 'region' => 'us' ), 'claude-searchbot' => array( 'ua' => 'Claude-SearchBot', 'vendor' => 'Anthropic', 'purpose' => 'search', 'region' => 'us' ), 'claude-user' => array( 'ua' => 'Claude-User', 'vendor' => 'Anthropic', 'purpose' => 'on-demand', 'region' => 'us' ), 'perplexitybot' => array( 'ua' => 'PerplexityBot', 'vendor' => 'Perplexity', 'purpose' => 'search', 'region' => 'us' ), 'perplexity-user' => array( 'ua' => 'Perplexity-User', 'vendor' => 'Perplexity', 'purpose' => 'on-demand', 'region' => 'us' ), 'google-extended' => array( 'ua' => 'Google-Extended', 'vendor' => 'Google', 'purpose' => 'training', 'region' => 'us' ), 'google-notebooklm' => array( 'ua' => 'Google-NotebookLM', 'vendor' => 'Google', 'purpose' => 'on-demand', 'region' => 'us' ), 'google-vertex' => array( 'ua' => 'Google-CloudVertexBot', 'vendor' => 'Google', 'purpose' => 'training', 'region' => 'us' ), 'applebot-extended' => array( 'ua' => 'Applebot-Extended', 'vendor' => 'Apple', 'purpose' => 'training', 'region' => 'us' ), 'meta-external' => array( 'ua' => 'meta-externalagent', 'vendor' => 'Meta', 'purpose' => 'training', 'region' => 'us' ), 'meta-fetcher' => array( 'ua' => 'Meta-ExternalFetcher', 'vendor' => 'Meta', 'purpose' => 'on-demand', 'region' => 'us' ), 'amazonbot' => array( 'ua' => 'Amazonbot', 'vendor' => 'Amazon', 'purpose' => 'search', 'region' => 'us' ), 'bedrockbot' => array( 'ua' => 'bedrockbot', 'vendor' => 'Amazon', 'purpose' => 'training', 'region' => 'us' ), 'cohere-ai' => array( 'ua' => 'cohere-ai', 'vendor' => 'Cohere', 'purpose' => 'training', 'region' => 'us' ), 'duckassistbot' => array( 'ua' => 'DuckAssistBot', 'vendor' => 'DuckDuckGo', 'purpose' => 'search', 'region' => 'us' ), 'ai2bot' => array( 'ua' => 'AI2Bot', 'vendor' => 'Allen Institute', 'purpose' => 'training', 'region' => 'us' ), 'ccbot' => array( 'ua' => 'CCBot', 'vendor' => 'Common Crawl', 'purpose' => 'training', 'region' => 'us' ), // ---- Europe ------------------------------------------------- 'mistralai-user' => array( 'ua' => 'MistralAI-User', 'vendor' => 'Mistral AI', 'purpose' => 'on-demand', 'region' => 'eu' ), 'yandexadditional' => array( 'ua' => 'YandexAdditional', 'vendor' => 'Yandex', 'purpose' => 'training', 'region' => 'eu' ), // ---- China -------------------------------------------------- // The Chinese assistants are the fastest-growing source of AI // answers outside the US, and several of them crawl far more // aggressively than their Western counterparts. 'bytespider' => array( 'ua' => 'Bytespider', 'vendor' => 'ByteDance (Doubao)', 'purpose' => 'training', 'region' => 'cn' ), 'tiktokspider' => array( 'ua' => 'TikTokSpider', 'vendor' => 'ByteDance', 'purpose' => 'training', 'region' => 'cn' ), 'petalbot' => array( 'ua' => 'PetalBot', 'vendor' => 'Huawei (Petal / Xiaoyi)', 'purpose' => 'search', 'region' => 'cn' ), 'pangubot' => array( 'ua' => 'PanguBot', 'vendor' => 'Huawei (Pangu)', 'purpose' => 'training', 'region' => 'cn' ), 'deepseekbot' => array( 'ua' => 'DeepSeekBot', 'vendor' => 'DeepSeek', 'purpose' => 'training', 'region' => 'cn', // Honest caveat: DeepSeek is widely reported to fetch without a // named identifier, so this rule is a best effort. Telling a // customer they have "blocked DeepSeek" would be a promise the // protocol cannot keep. 'note' => 'unreliable', ), 'baiduspider' => array( 'ua' => 'Baiduspider', 'vendor' => 'Baidu (ERNIE)', 'purpose' => 'search', 'region' => 'cn' ), 'yisouspider' => array( 'ua' => 'YisouSpider', 'vendor' => 'Alibaba / UC (Quark, Qwen)', 'purpose' => 'search', 'region' => 'cn' ), 'sogouspider' => array( 'ua' => 'Sogou web spider', 'vendor' => 'Tencent / Sogou (Yuanbao)', 'purpose' => 'search', 'region' => 'cn' ), '360spider' => array( 'ua' => '360Spider', 'vendor' => '360 (Nano AI)', 'purpose' => 'search', 'region' => 'cn' ), ); /** * Filter the AI bot registry. * * @param array $bots Bot definitions keyed by id. */ return (array) apply_filters( 'airanksuite/crawler_controls/bots', $bots ); } /** * Default settings: everything allowed (site owner opts into blocking). * * @return array */ public static function defaults(): array { return array( 'enabled' => true, 'blocked' => array(), // Record which crawlers arrive. Off by default: it writes an // option per crawler request, and that is a decision to make // knowingly rather than one a plugin makes for you. 'log' => false, // robots.txt is a request, not a fence. `enforce` refuses the // request outright for bots that ignore it. 'enforce' => false, ); } public function register( Container $container ): void {} public function boot( Container $container ): void { Guard::filter( 'robots_txt', array( $this, 'robots_rules' ), 20, 1 ); // Front-end only, and as early as a request can be identified. Not on // admin, cron or REST: none of those are ever a crawler, and checking // on every one of them would be a tax on the admin for nothing. Guard::action( 'parse_request', array( $this, 'watch' ), 1 ); add_action( 'rest_api_init', static function () use ( $container ): void { ( new Rest\CrawlersController( $container->get( Options::class ) ) )->register_routes(); } ); } /** * Note an AI crawler's visit, and turn it away if asked to. * * This is the difference between a robots.txt rule and an actual block. * A crawler that ignores robots.txt — and several on the list are widely * reported to — is unaffected by every switch on the settings screen. A * 403 is not. */ public function watch(): void { if ( is_admin() || wp_doing_cron() || wp_doing_ajax() ) { return; } if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return; } $settings = \AiRankSuite\Kernel::service( Options::class )->get( 'crawler_controls', self::defaults() ); if ( empty( $settings['log'] ) && empty( $settings['enforce'] ) ) { return; } $agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $bot = Services\CrawlerLog::identify( $agent ); if ( null === $bot ) { return; } $blocked = in_array( $bot, (array) $settings['blocked'], true ); $refuse = $blocked && ! empty( $settings['enforce'] ) && ! empty( $settings['enabled'] ); $path = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( (string) $_SERVER['REQUEST_URI'] ) ) : '/'; if ( ! empty( $settings['log'] ) ) { Services\CrawlerLog::record( $bot, $path, $refuse ? 403 : 200 ); } if ( $refuse ) { // 403 rather than 404: a crawler told the page does not exist may // drop the URL permanently, which is a different and worse outcome // than being told it is not allowed to have it. status_header( 403 ); nocache_headers(); header( 'Content-Type: text/plain; charset=utf-8' ); echo esc_html__( 'This crawler is not permitted on this site.', 'ai-rank-suite' ); exit; } } /** * Append managed per-bot rules to the virtual robots.txt. * * Note: a physical robots.txt file bypasses this filter — the admin UI * warns when one exists. * * @param string $output Current robots.txt content. */ public function robots_rules( string $output ): string { $container = \AiRankSuite\Kernel::instance()->container(); $settings = $container->get( Options::class )->get( 'crawler_controls', self::defaults() ); if ( empty( $settings['enabled'] ) ) { return $output; } $blocked = (array) $settings['blocked']; if ( empty( $blocked ) ) { return $output; } $lines = array( '', '# Managed by AI Rank Suite — AI crawler controls' ); $bots = self::bots(); foreach ( $blocked as $bot_id ) { if ( ! isset( $bots[ $bot_id ] ) ) { continue; } $lines[] = 'User-agent: ' . $bots[ $bot_id ]['ua']; $lines[] = 'Disallow: /'; $lines[] = ''; } return $output . implode( "\n", $lines ); } }