prefix . 'aije_ai_visits'; } /* ===== Bekannte KI-Crawler ===== * Schlüssel = Teilstring im User-Agent (case-insensitive). * Spezifischere Muster stehen VOR allgemeineren, da der erste Treffer zählt. * purpose: training = sammelt Trainingsdaten, search = KI-Suchindex, * user = Live-Abruf, weil ein Nutzer im Chat gerade danach fragt. */ public static function ai_bot_list() { return array( // OpenAI 'OAI-SearchBot' => array( 'name' => 'OAI-SearchBot', 'vendor' => 'OpenAI', 'purpose' => 'search' ), 'ChatGPT-User' => array( 'name' => 'ChatGPT-User', 'vendor' => 'OpenAI', 'purpose' => 'user' ), 'GPTBot' => array( 'name' => 'GPTBot', 'vendor' => 'OpenAI', 'purpose' => 'training' ), // Anthropic 'Claude-SearchBot' => array( 'name' => 'Claude-SearchBot', 'vendor' => 'Anthropic', 'purpose' => 'search' ), 'Claude-User' => array( 'name' => 'Claude-User', 'vendor' => 'Anthropic', 'purpose' => 'user' ), 'ClaudeBot' => array( 'name' => 'ClaudeBot', 'vendor' => 'Anthropic', 'purpose' => 'training' ), 'Claude-Web' => array( 'name' => 'Claude-Web', 'vendor' => 'Anthropic', 'purpose' => 'user' ), 'anthropic-ai' => array( 'name' => 'anthropic-ai', 'vendor' => 'Anthropic', 'purpose' => 'training' ), // Perplexity 'Perplexity-User' => array( 'name' => 'Perplexity-User', 'vendor' => 'Perplexity', 'purpose' => 'user' ), 'PerplexityBot' => array( 'name' => 'PerplexityBot', 'vendor' => 'Perplexity', 'purpose' => 'search' ), // Google (Gemini / Vertex AI) 'Google-CloudVertexBot' => array( 'name' => 'Google-CloudVertexBot', 'vendor' => 'Google', 'purpose' => 'user' ), 'GoogleAgent' => array( 'name' => 'Google Agent', 'vendor' => 'Google', 'purpose' => 'user' ), // Meta 'meta-externalfetcher' => array( 'name' => 'Meta-ExternalFetcher', 'vendor' => 'Meta', 'purpose' => 'user' ), 'meta-externalagent' => array( 'name' => 'Meta-ExternalAgent', 'vendor' => 'Meta', 'purpose' => 'training' ), // Microsoft / Apple / Amazon 'Applebot' => array( 'name' => 'Applebot', 'vendor' => 'Apple', 'purpose' => 'search' ), 'Amazonbot' => array( 'name' => 'Amazonbot', 'vendor' => 'Amazon', 'purpose' => 'search' ), // Weitere 'MistralAI-User' => array( 'name' => 'MistralAI-User', 'vendor' => 'Mistral', 'purpose' => 'user' ), 'DuckAssistBot' => array( 'name' => 'DuckAssistBot', 'vendor' => 'DuckDuckGo', 'purpose' => 'user' ), 'YouBot' => array( 'name' => 'YouBot', 'vendor' => 'You.com', 'purpose' => 'search' ), 'cohere-training-data-crawler' => array( 'name' => 'Cohere Training', 'vendor' => 'Cohere', 'purpose' => 'training' ), 'cohere-ai' => array( 'name' => 'cohere-ai', 'vendor' => 'Cohere', 'purpose' => 'user' ), 'Bytespider' => array( 'name' => 'Bytespider', 'vendor' => 'ByteDance', 'purpose' => 'training' ), 'CCBot' => array( 'name' => 'CCBot', 'vendor' => 'Common Crawl', 'purpose' => 'training' ), 'AI2Bot' => array( 'name' => 'AI2Bot', 'vendor' => 'Allen AI', 'purpose' => 'training' ), 'Diffbot' => array( 'name' => 'Diffbot', 'vendor' => 'Diffbot', 'purpose' => 'training' ), 'Timpibot' => array( 'name' => 'Timpibot', 'vendor' => 'Timpi', 'purpose' => 'training' ), 'omgili' => array( 'name' => 'Omgili', 'vendor' => 'Webz.io', 'purpose' => 'training' ), ); } /* ===== KI-Chats als Traffic-Quelle ===== * Erkennung echter Besucher, die in einer KI-Antwort auf einen Link geklickt * haben – über den Referrer (Host) oder den utm_source-Parameter, den z. B. * ChatGPT automatisch anhängt. Gespeichert werden nur Quelle, Seite, Zeit. */ public static function ai_referral_sources() { return array( // host => [name, vendor, utm-Stichwort] 'chatgpt.com' => array( 'ChatGPT', 'OpenAI', 'chatgpt' ), 'chat.openai.com' => array( 'ChatGPT', 'OpenAI', 'openai' ), 'perplexity.ai' => array( 'Perplexity', 'Perplexity', 'perplexity' ), 'gemini.google.com' => array( 'Gemini', 'Google', 'gemini' ), 'bard.google.com' => array( 'Gemini', 'Google', 'bard' ), 'claude.ai' => array( 'Claude', 'Anthropic', 'claude' ), 'copilot.microsoft.com' => array( 'Copilot', 'Microsoft', 'copilot' ), 'copilot.cloud.microsoft' => array( 'Copilot', 'Microsoft', 'copilot' ), 'chat.mistral.ai' => array( 'Le Chat', 'Mistral', 'mistral' ), 'chat.deepseek.com' => array( 'DeepSeek', 'DeepSeek', 'deepseek' ), 'grok.com' => array( 'Grok', 'xAI', 'grok' ), 'meta.ai' => array( 'Meta AI', 'Meta', 'meta.ai' ), 'you.com' => array( 'You.com', 'You.com', 'you.com' ), 'phind.com' => array( 'Phind', 'Phind', 'phind' ), ); } public static function detect_ai_referral() { $sources = self::ai_referral_sources(); // 1) utm_source (z. B. ?utm_source=chatgpt.com) if ( ! empty( $_GET['utm_source'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification -- nur lesend $utm = strtolower( sanitize_text_field( wp_unslash( $_GET['utm_source'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification foreach ( $sources as $s ) { if ( false !== strpos( $utm, $s[2] ) ) { return array( 'name' => $s[0], 'vendor' => $s[1] ); } } } // 2) Referrer-Host if ( ! empty( $_SERVER['HTTP_REFERER'] ) ) { $host = strtolower( (string) wp_parse_url( esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ), PHP_URL_HOST ) ); if ( '' !== $host ) { foreach ( $sources as $domain => $s ) { if ( $host === $domain || substr( $host, -strlen( '.' . $domain ) ) === '.' . $domain ) { return array( 'name' => $s[0], 'vendor' => $s[1] ); } } } } return null; } public static function ai_purpose_labels() { return array( 'training' => __( 'Training', 'ai-json-extractor-auto-embed-lite' ), 'search' => __( 'KI-Suche', 'ai-json-extractor-auto-embed-lite' ), 'user' => __( 'Live-Abruf (Chat)', 'ai-json-extractor-auto-embed-lite' ), ); } /** Liefert den Bot-Eintrag für einen User-Agent oder null. */ public static function detect_ai_bot( $ua ) { if ( '' === $ua ) { return null; } // Exakte User-Agents (Teilstring-Suche wäre hier zu breit). // Gemini ruft Seiten live mit dem nackten UA "Google" ab – per Debug-Modus // am 23.09.2026 auf wprankify.de nachgewiesen. Nur exakt, damit Googlebot // & Co. ("Mozilla/5.0 (compatible; Googlebot/2.1 …)") NICHT mitgezählt werden. $exact = array( 'Google' => array( 'name' => 'Gemini', 'vendor' => 'Google', 'purpose' => 'user' ), ); $ua_trim = trim( $ua ); if ( isset( $exact[ $ua_trim ] ) ) { return $exact[ $ua_trim ]; } foreach ( self::ai_bot_list() as $needle => $bot ) { if ( false !== stripos( $ua, $needle ) ) { return $bot; } } return null; } /* ===== Tabelle anlegen / aktualisieren ===== * Läuft auf admin_init – so bekommen auch Bestandsinstallationen die Tabelle, * die per Update (ohne erneute Aktivierung) auf diese Version kommen. */ public function maybe_install_ai_visits_table() { if ( get_option( 'aije_ai_visits_db' ) === self::$ai_visits_db_version ) { return; } global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $table = self::ai_visits_table(); $charset = $wpdb->get_charset_collate(); $sql = "CREATE TABLE {$table} ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, visited_at DATETIME NOT NULL, bot VARCHAR(64) NOT NULL, vendor VARCHAR(64) NOT NULL, purpose VARCHAR(16) NOT NULL, post_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, url VARCHAR(500) NOT NULL, status SMALLINT(5) UNSIGNED NOT NULL DEFAULT 200, PRIMARY KEY (id), KEY visited_at (visited_at), KEY bot (bot), KEY post_id (post_id) ) {$charset};"; dbDelta( $sql ); update_option( 'aije_ai_visits_db', self::$ai_visits_db_version, false ); } /* ===== Besuch protokollieren (Frontend, template_redirect Prio 0) ===== */ public function log_ai_visit() { if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) { return; } if ( ! get_option( 'aije_ai_visits_enabled', 1 ) ) { return; } if ( get_option( 'aije_ai_visits_db' ) !== self::$ai_visits_db_version ) { return; } $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $bot = self::detect_ai_bot( $ua ); $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/'; $path = substr( (string) wp_parse_url( $uri, PHP_URL_PATH ), 0, 500 ); if ( '' === $path ) { $path = '/'; } // Debug-Modus: für begrenzte Zeit JEDEN User-Agent mitschreiben. if ( self::ai_debug_active() && ! is_user_logged_in() ) { $this->log_ai_debug( $ua, $path, $bot ); } // Kein Crawler → prüfen, ob ein echter Besucher aus einem KI-Chat kommt. $referral = null; if ( ! $bot ) { if ( is_user_logged_in() ) { return; } $referral = self::detect_ai_referral(); if ( ! $referral ) { return; } $bot = array( 'name' => $referral['name'], 'vendor' => $referral['vendor'], 'purpose' => 'referral' ); } $post_id = is_singular() ? (int) get_queried_object_id() : 0; if ( ! $post_id && ( is_front_page() || is_home() ) ) { $post_id = (int) get_option( 'page_on_front' ); } global $wpdb; $wpdb->insert( self::ai_visits_table(), array( 'visited_at' => current_time( 'mysql' ), 'bot' => $bot['name'], 'vendor' => $bot['vendor'], 'purpose' => $bot['purpose'], 'post_id' => $post_id, 'url' => $path, 'status' => is_404() ? 404 : 200, ), array( '%s', '%s', '%s', '%s', '%d', '%s', '%d' ) ); // Aufräumen ohne eigenen Cron: bei ca. jedem 50. Bot-Besuch alte Einträge löschen. if ( 1 === wp_rand( 1, 50 ) ) { $this->purge_old_ai_visits(); } } public function purge_old_ai_visits() { global $wpdb; $days = max( 7, (int) get_option( 'aije_ai_visits_retention', 90 ) ); $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . self::ai_visits_table() . ' WHERE visited_at < %s', gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) - $days * DAY_IN_SECONDS ) ) ); } /* ===== Debug-Modus ===== * Schreibt für 15 Minuten den User-Agent JEDES nicht eingeloggten Aufrufs mit, * um unbekannte KI-Abrufer (z. B. Gemini) zu identifizieren. Keine IPs. * Speicherung als nicht-autoloadete Option, max. 300 Einträge. */ const AI_DEBUG_MINUTES = 15; const AI_DEBUG_MAX = 300; public static function ai_debug_active() { return (int) get_option( 'aije_ai_debug_until', 0 ) > time(); } private function log_ai_debug( $ua, $path, $bot ) { $log = get_option( 'aije_ai_debug_log', array() ); if ( ! is_array( $log ) ) { $log = array(); } if ( count( $log ) >= self::AI_DEBUG_MAX ) { return; } $log[] = array( 't' => current_time( 'mysql' ), 'ua' => substr( $ua, 0, 400 ), 'path' => $path, 'bot' => $bot ? $bot['name'] : '', ); update_option( 'aije_ai_debug_log', $log, false ); } /* ===== Admin-Aktionen (Einstellungen speichern / Log leeren / Debug) ===== */ public function handle_ai_visits_settings() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Keine Berechtigung.', 'ai-json-extractor-auto-embed-lite' ) ); } if ( ! isset( $_POST['aije_ai_visits_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['aije_ai_visits_nonce'] ) ), 'aije_ai_visits' ) ) { wp_die( esc_html__( 'Sicherheitsprüfung fehlgeschlagen.', 'ai-json-extractor-auto-embed-lite' ) ); } $msg = 'saved'; $debug = isset( $_POST['aije_debug'] ) ? sanitize_key( $_POST['aije_debug'] ) : ''; if ( 'start' === $debug ) { delete_option( 'aije_ai_debug_log' ); update_option( 'aije_ai_debug_until', time() + self::AI_DEBUG_MINUTES * MINUTE_IN_SECONDS, false ); $msg = 'debug_on'; } elseif ( 'stop' === $debug ) { update_option( 'aije_ai_debug_until', 0, false ); $msg = 'debug_off'; } elseif ( 'clear' === $debug ) { delete_option( 'aije_ai_debug_log' ); update_option( 'aije_ai_debug_until', 0, false ); $msg = 'debug_cleared'; } elseif ( ! empty( $_POST['aije_clear_log'] ) ) { global $wpdb; $wpdb->query( 'TRUNCATE TABLE ' . self::ai_visits_table() ); $msg = 'cleared'; } else { update_option( 'aije_ai_visits_enabled', ! empty( $_POST['ai_visits_enabled'] ) ? 1 : 0 ); $ret = isset( $_POST['ai_visits_retention'] ) ? (int) $_POST['ai_visits_retention'] : 90; update_option( 'aije_ai_visits_retention', max( 7, min( 365, $ret ) ) ); } wp_safe_redirect( add_query_arg( array( 'page' => 'aije-lite', 'tab' => 'aivisits', 'aije_msg' => $msg ), admin_url( 'tools.php' ) ) ); exit; } }