token => display name */ private static function ai_tokens() { return array( 'ChatGPT-User' => 'ChatGPT', 'Claude-User' => 'Claude', 'Perplexity-User' => 'Perplexity', 'Google-Agent' => 'Google Agent', 'Meta-ExternalFetcher' => 'Meta', 'MistralAI-User' => 'Mistral', 'DuckAssistBot' => 'DuckDuckGo', 'GrokAgent' => 'Grok', ); } /** * AI crawlers. They index or train. They do not check out, so a checkout that names one is a claim. * * @return array token => display name */ public static function ai_crawler_tokens() { return array( 'OAI-SearchBot' => 'OpenAI Search', 'OAI-AdsBot' => 'OpenAI Ads', 'GPTBot' => 'OpenAI GPTBot', 'Claude-SearchBot' => 'Claude Search', 'ClaudeBot' => 'ClaudeBot', 'anthropic-ai' => 'anthropic-ai', 'PerplexityBot' => 'PerplexityBot', 'Google-Extended' => 'Google-Extended', 'GoogleOther' => 'GoogleOther', 'Applebot-Extended' => 'Applebot-Extended', 'Meta-ExternalAgent' => 'Meta crawler', 'Amazonbot' => 'Amazonbot', 'Bytespider' => 'Bytespider', 'cohere-ai' => 'Cohere', 'CCBot' => 'Common Crawl', 'Diffbot' => 'Diffbot', 'YouBot' => 'You.com', ); } /** * @param string $user_agent * @return string Crawler display name, or empty. */ public static function match_ai_crawler( $user_agent ) { foreach ( self::ai_crawler_tokens() as $token => $name ) { if ( self::contains_token( $user_agent, $token ) ) { return $name; } } return ''; } /** * Crawlers that are not shopping agents. They must not be stamped Human. * * @return string[] */ public static function other_bot_tokens() { return array( 'Googlebot', 'AdsBot-Google', 'Bingbot', 'DuckDuckBot', 'Baiduspider', 'YandexBot', 'AhrefsBot', 'SemrushBot', 'PetalBot', 'facebookexternalhit', 'Twitterbot', 'LinkedInBot', 'Slackbot', 'Embedly', 'Qwantify', ); } /** * @param string $user_agent * @return string Display name, or empty when the UA is not a published agent. */ public static function match_ai_ua( $user_agent ) { foreach ( self::ai_tokens() as $token => $name ) { if ( self::contains_token( $user_agent, $token ) ) { return $name; } } $prefixed = self::match_agent_prefix( $user_agent ); if ( '' !== $prefixed ) { return $prefixed; } return ''; } /** * Amazon's agent form: Agent/Muse. A URL path /Agent/ does not count. * GrokAgent/1.0 is named from the token list, not as "1.0". * * @param string $user_agent * @return string */ private static function match_agent_prefix( $user_agent ) { if ( ! is_string( $user_agent ) || '' === $user_agent ) { return ''; } if ( ! preg_match( '/(?:^|[;(\s])Agent\/([A-Za-z][A-Za-z0-9._-]{0,40})/i', $user_agent, $match ) ) { return ''; } return $match[1]; } /** * @param string $user_agent * @return bool */ public static function is_other_bot( $user_agent ) { foreach ( self::other_bot_tokens() as $token ) { if ( self::contains_token( $user_agent, $token ) ) { return true; } } return false; } /** * @param string $user_agent * @return bool */ public static function is_human_browser( $user_agent ) { if ( ! is_string( $user_agent ) || strlen( $user_agent ) < 20 ) { return false; } if ( 0 !== strpos( $user_agent, 'Mozilla/5.0' ) ) { return false; } $browser = false; foreach ( array( 'Chrome/', 'CriOS/', 'Safari/', 'Firefox/', 'FxiOS/', 'Edg/', 'OPR/', 'SamsungBrowser/' ) as $token ) { if ( false !== strpos( $user_agent, $token ) ) { $browser = true; break; } } // In-app browsers (Instagram, Facebook, Google app, TikTok, Pinterest, Snapchat, LINE) leave out // "Safari/" on iOS. They are people shopping from a link. if ( ! $browser && false !== strpos( $user_agent, 'AppleWebKit/' ) && preg_match( '/(?:FBAN|FBAV|Instagram|GSA\/|Line\/|Pinterest|Snapchat|TikTok|musical_ly|BytedanceWebview)/', $user_agent ) ) { $browser = true; } if ( ! $browser ) { return false; } foreach ( array( 'HeadlessChrome', 'Headless', 'PhantomJS', 'Selenium', 'Puppeteer', 'Playwright' ) as $marker ) { if ( false !== stripos( $user_agent, $marker ) ) { return false; } } return true; } /** * Display name for the host that published the signing key. * * @param string $host * @return string */ public static function name_for_host( $host ) { $host = strtolower( (string) $host ); $map = array( 'openai.com' => 'ChatGPT', 'chatgpt.com' => 'ChatGPT', 'oaistatic.com' => 'ChatGPT', 'anthropic.com' => 'Claude', 'claude.ai' => 'Claude', 'perplexity.ai' => 'Perplexity', 'google.com' => 'Google', 'googlebot.com' => 'Google', 'agent.bot.goog' => 'Google', 'agentpay-key-directory.mastercard.com' => 'Mastercard Agent Pay', 'mastercard.com' => 'Mastercard Agent Pay', 'visa.com' => 'Visa Intelligent Commerce', 'amazon.com' => 'Amazon', 'amazonbot.amazon' => 'Amazon', 'facebook.com' => 'Meta', 'meta.com' => 'Meta', 'apple.com' => 'Apple', 'bytedance.com' => 'ByteDance', ); foreach ( $map as $suffix => $name ) { if ( $host === $suffix || substr( $host, -strlen( '.' . $suffix ) ) === '.' . $suffix ) { return $name; } } return $host; } /** * @param string $haystack * @param string $token * @return bool */ private static function contains_token( $haystack, $token ) { if ( ! is_string( $haystack ) || '' === $haystack ) { return false; } return false !== stripos( $haystack, $token ); } }