|null */ private $pending = null; public function __construct() { add_action( 'init', array( $this, 'detect_request' ), 5 ); } /** * Detect whether the current request is an AI crawler and, if so, queue a write. */ public function detect_request() { if ( ! $this->should_track() ) { return; } $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; if ( '' === $user_agent ) { return; } $bot = Aiso_Bots::detect( $user_agent ); $track_all = (bool) Aiso_Analytics::option( 'track_all_bots', false ); if ( null === $bot && ! $track_all ) { return; // Not an AI crawler and "track everything" is off — do nothing. } $ip = $this->client_ip(); $company = $bot ? $bot['company'] : ''; $verified = ( $bot && '' !== $ip ) ? Aiso_Bots::verify_ip( $ip, $company ) : null; $this->pending = array( 'user_agent' => $user_agent, 'bot' => $bot, 'ip' => $ip, 'verified' => $verified, 'path' => $this->request_path(), 'url' => $this->request_url(), 'method' => isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : 'GET', 'referer' => isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', 'country' => $this->edge_country(), ); add_action( 'shutdown', array( $this, 'record' ), 0 ); } /** * Persist the queued hit, then opportunistically flush to MongoDB. */ public function record() { if ( null === $this->pending ) { return; } $p = $this->pending; $bot = $p['bot']; $anonymize = (bool) Aiso_Analytics::option( 'anonymize_ip', true ); $ip = $p['ip']; $ip_hash = '' !== $ip ? hash( 'sha256', self::ip_salt() . $ip ) : ''; $stored_ip = $anonymize ? '' : $ip; Aiso_Store::insert_hit( array( 'hit_time' => gmdate( 'Y-m-d H:i:s' ), 'bot_slug' => $bot ? $bot['slug'] : 'other', 'bot_name' => $bot ? $bot['name'] : 'Other / Unknown', 'company' => $bot ? $bot['company'] : '', 'platform' => $bot ? $bot['platform'] : 'other', 'function_tag' => $bot ? $bot['function'] : '', 'is_impression' => $bot ? ! empty( $bot['is_impression'] ) : false, 'path' => $p['path'], 'url' => $p['url'], 'method' => $p['method'], 'status' => (int) http_response_code(), 'user_agent' => $p['user_agent'], 'ip' => $stored_ip, 'ip_hash' => $ip_hash, 'country' => $p['country'], 'referer' => $p['referer'], 'verified' => $p['verified'], ) ); $this->pending = null; // Ship to the central Aiso store after the response has been flushed to the // visitor, so tracking never adds latency to the page load on PHP-FPM hosts. if ( Aiso_Analytics::instance()->sync && Aiso_Analytics::instance()->sync->is_connected() ) { if ( function_exists( 'fastcgi_finish_request' ) ) { fastcgi_finish_request(); } Aiso_Analytics::instance()->sync->maybe_flush_on_shutdown(); } } /** * Should the current request be considered for tracking at all? * * @return bool */ private function should_track() { if ( is_admin() ) { return false; } if ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) { return false; } if ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) { return false; } if ( defined( 'WP_CLI' ) && WP_CLI ) { return false; } if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) { return false; } if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return false; } $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET'; return in_array( $method, array( 'GET', 'HEAD' ), true ); } /** * Request path without the query string. * * @return string */ private function request_path() { $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/'; $uri = wp_parse_url( $uri, PHP_URL_PATH ); return $uri ? $uri : '/'; } /** * Full requested URL. * * @return string */ private function request_url() { $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/'; return esc_url_raw( home_url( $uri ) ); } /** * Best-effort real client IP, honouring common CDN headers. * * @return string */ private function client_ip() { $candidates = array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR' ); foreach ( $candidates as $key ) { if ( empty( $_SERVER[ $key ] ) ) { continue; } $raw = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ); // X-Forwarded-For may be a comma-separated chain; the client is first. $first = trim( explode( ',', $raw )[0] ); if ( filter_var( $first, FILTER_VALIDATE_IP ) ) { return $first; } } return ''; } /** * Country from a CDN edge header, if the site sits behind one. * * @return string Two-letter country code, or ''. */ private function edge_country() { foreach ( array( 'HTTP_CF_IPCOUNTRY', 'HTTP_CLOUDFRONT_VIEWER_COUNTRY', 'HTTP_X_VERCEL_IP_COUNTRY' ) as $key ) { if ( ! empty( $_SERVER[ $key ] ) ) { $cc = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ) ); if ( preg_match( '/^[A-Z]{2}$/', $cc ) ) { return $cc; } } } return ''; } /** * Per-site salt for hashing IPs (privacy mode). * * @return string */ private static function ip_salt() { $salt = get_option( 'aiso_ip_salt' ); if ( ! $salt ) { $salt = wp_generate_password( 32, false, false ); update_option( 'aiso_ip_salt', $salt, false ); } return $salt; } }