wpmind/modules/geo/includes/CrawlerTracker.php
LinuxJoy adb6c49301 style: phpcbf 自动格式化 — 全量 WPCS 3.x 规范对齐
135 文件 19,211 处自动修复:空格→Tab 缩进、括号间距、
函数声明空格、前置自增、尾逗号等纯格式化改动。
零逻辑变更,php -l + token 级对比验证通过。

新增 phpcs.xml.dist / phpstan.neon.dist 项目配置。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 00:48:46 +08:00

289 lines
6.8 KiB
PHP

<?php
/**
* AI Crawler Tracker
*
* Tracks AI crawler access to Markdown feeds for analytics.
*
* @package WPMind\Modules\Geo
* @since 3.0.0
*/
declare(strict_types=1);
namespace WPMind\Modules\Geo;
/**
* Class CrawlerTracker
*
* Identifies and tracks AI crawler access patterns.
*/
class CrawlerTracker {
/**
* Known AI crawler user agents.
*
* @var array
*/
private const AI_CRAWLERS = array(
'GPTBot' => 'OpenAI',
'ChatGPT-User' => 'OpenAI',
'Google-Extended' => 'Google AI',
'Googlebot' => 'Google',
'Bingbot' => 'Microsoft',
'ClaudeBot' => 'Anthropic',
'Claude-Web' => 'Anthropic',
'PerplexityBot' => 'Perplexity',
'Bytespider' => 'ByteDance',
'CCBot' => 'Common Crawl',
'anthropic-ai' => 'Anthropic',
'cohere-ai' => 'Cohere',
'Applebot' => 'Apple',
'FacebookBot' => 'Meta',
'Meta-ExternalAgent' => 'Meta',
'Amazonbot' => 'Amazon',
'Baiduspider' => '百度',
'Sogou web spider' => '搜狗',
'360Spider' => '360',
'YisouSpider' => '神马搜索',
'DuckAssistBot' => 'DuckDuckGo',
);
/**
* Constructor.
*/
public function __construct() {
if ( ! get_option( 'wpmind_crawler_tracking', true ) ) {
return;
}
// Track markdown feed access.
add_action( 'wpmind_markdown_feed_accessed', array( $this, 'track_access' ) );
// Hook into feed rendering.
add_action( 'do_feed_markdown', array( $this, 'on_feed_access' ), 1 );
}
/**
* Called when markdown feed is accessed.
*/
public function on_feed_access(): void {
$this->track_access();
}
/**
* Track crawler access.
*/
public function track_access(): void {
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] )
? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
: '';
$crawler_info = $this->identify_crawler( $user_agent );
if ( ! $crawler_info ) {
return;
}
// Log the access.
$this->log_access( $crawler_info, $user_agent );
// Update statistics.
$this->update_stats( $crawler_info );
}
/**
* Identify crawler from user agent.
*
* @param string $user_agent The user agent string.
* @return array|null Crawler info or null if not identified.
*/
public function identify_crawler( string $user_agent ): ?array {
foreach ( self::AI_CRAWLERS as $pattern => $company ) {
if ( false !== stripos( $user_agent, $pattern ) ) {
return array(
'pattern' => $pattern,
'company' => $company,
'is_ai' => $this->is_ai_crawler( $pattern ),
);
}
}
return null;
}
/**
* Check if crawler is specifically an AI crawler.
*
* @param string $pattern The crawler pattern.
* @return bool True if AI crawler.
*/
private function is_ai_crawler( string $pattern ): bool {
$ai_specific = array(
'GPTBot',
'ChatGPT-User',
'Google-Extended',
'ClaudeBot',
'Claude-Web',
'PerplexityBot',
'anthropic-ai',
'cohere-ai',
'Bytespider',
'DuckAssistBot',
);
return in_array( $pattern, $ai_specific, true );
}
/**
* Log crawler access.
*
* @param array $crawler_info Crawler information.
* @param string $user_agent Full user agent string.
*/
private function log_access( array $crawler_info, string $user_agent ): void {
$log_entry = array(
'timestamp' => current_time( 'mysql' ),
'crawler' => $crawler_info['pattern'],
'company' => $crawler_info['company'],
'is_ai' => $crawler_info['is_ai'],
'user_agent' => $user_agent,
'url' => isset( $_SERVER['REQUEST_URI'] )
? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) )
: '',
'ip' => $this->get_client_ip(),
);
// Get existing logs.
$logs = get_option( 'wpmind_crawler_logs', array() );
$logs[] = $log_entry;
// Keep only last 1000 entries.
if ( count( $logs ) > 1000 ) {
$logs = array_slice( $logs, -1000 );
}
update_option( 'wpmind_crawler_logs', $logs, false );
}
/**
* Update crawler statistics.
*
* @param array $crawler_info Crawler information.
*/
private function update_stats( array $crawler_info ): void {
$stats = get_option( 'wpmind_crawler_stats', array() );
$key = $crawler_info['pattern'];
if ( ! isset( $stats[ $key ] ) ) {
$stats[ $key ] = array(
'company' => $crawler_info['company'],
'is_ai' => $crawler_info['is_ai'],
'total_hits' => 0,
'first_seen' => current_time( 'mysql' ),
'last_seen' => current_time( 'mysql' ),
'daily_hits' => array(),
);
}
++$stats[ $key ]['total_hits'];
$stats[ $key ]['last_seen'] = current_time( 'mysql' );
// Track daily hits.
$today = current_time( 'Y-m-d' );
if ( ! isset( $stats[ $key ]['daily_hits'][ $today ] ) ) {
$stats[ $key ]['daily_hits'][ $today ] = 0;
}
++$stats[ $key ]['daily_hits'][ $today ];
// Keep only last 30 days.
$stats[ $key ]['daily_hits'] = array_slice(
$stats[ $key ]['daily_hits'],
-30,
30,
true
);
update_option( 'wpmind_crawler_stats', $stats, false );
}
/**
* Get client IP address.
*
* @return string Client IP.
*/
private function get_client_ip(): string {
$ip_keys = array(
'HTTP_CF_CONNECTING_IP', // Cloudflare.
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'REMOTE_ADDR',
);
foreach ( $ip_keys as $key ) {
if ( ! empty( $_SERVER[ $key ] ) ) {
$ip = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
// Handle comma-separated IPs.
if ( false !== strpos( $ip, ',' ) ) {
$ip = trim( explode( ',', $ip )[0] );
}
if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
return $ip;
}
}
}
return 'unknown';
}
/**
* Get crawler statistics.
*
* @return array Crawler statistics.
*/
public function get_stats(): array {
return get_option( 'wpmind_crawler_stats', array() );
}
/**
* Get recent crawler logs.
*
* @param int $limit Number of logs to return.
* @return array Recent logs.
*/
public function get_recent_logs( int $limit = 100 ): array {
$logs = get_option( 'wpmind_crawler_logs', array() );
return array_slice( $logs, -$limit );
}
/**
* Get AI crawler summary.
*
* @return array Summary of AI crawler activity.
*/
public function get_ai_summary(): array {
$stats = $this->get_stats();
$summary = array(
'total_ai_hits' => 0,
'total_search_hits' => 0,
'ai_crawlers' => array(),
'search_crawlers' => array(),
);
foreach ( $stats as $crawler => $data ) {
if ( $data['is_ai'] ) {
$summary['total_ai_hits'] += $data['total_hits'];
$summary['ai_crawlers'][ $crawler ] = $data['total_hits'];
} else {
$summary['total_search_hits'] += $data['total_hits'];
$summary['search_crawlers'][ $crawler ] = $data['total_hits'];
}
}
// Sort by hits.
arsort( $summary['ai_crawlers'] );
arsort( $summary['search_crawlers'] );
return $summary;
}
}