prefix . self::TABLE; $charset = $wpdb->get_charset_collate(); $sql = "CREATE TABLE {$table} ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, bot VARCHAR(64) NOT NULL, url VARCHAR(2048) NOT NULL, visited_at DATETIME NOT NULL, PRIMARY KEY (id), KEY bot (bot), KEY visited_at (visited_at) ) {$charset};"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( $sql ); } public function maybe_log() { if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) { return; } $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; $bot = ATBA_Bots::match( $ua ); if ( ! $bot ) { return; } $uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/'; $uri = substr( $uri, 0, 2048 ); global $wpdb; $table = $wpdb->prefix . self::TABLE; // Throttle: same bot + same path logged at most once per 10 minutes. // Checked against the log table itself (indexed, size-bounded by the // retention cron) instead of transients, so unauthenticated requests // cannot bloat the options table with unique keys. $recent = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE bot = %s AND url = %s AND visited_at > DATE_SUB(NOW(), INTERVAL 10 MINUTE) LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $bot, $uri ) ); if ( $recent ) { return; } $wpdb->insert( $table, array( 'bot' => $bot, 'url' => $uri, 'visited_at' => current_time( 'mysql' ), ), array( '%s', '%s', '%s' ) ); } /** Keep the table small: retention configurable, default 90 days. */ public function cleanup() { global $wpdb; $days = (int) apply_filters( 'atba_retention_days', get_option( 'atba_retention', 90 ) ); $table = $wpdb->prefix . self::TABLE; $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE visited_at < DATE_SUB(NOW(), INTERVAL %d DAY)", $days ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared ); } /** Summary counts per bot for the last N days. */ public static function stats( $days = 30 ) { global $wpdb; $table = $wpdb->prefix . self::TABLE; return $wpdb->get_results( $wpdb->prepare( "SELECT bot, COUNT(*) AS hits, MAX(visited_at) AS last_seen FROM {$table} WHERE visited_at >= DATE_SUB(NOW(), INTERVAL %d DAY) GROUP BY bot ORDER BY hits DESC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $days ) ); } /** Most crawled URLs for the last N days. */ public static function top_urls( $days = 30, $limit = 10 ) { global $wpdb; $table = $wpdb->prefix . self::TABLE; return $wpdb->get_results( $wpdb->prepare( "SELECT url, COUNT(*) AS hits FROM {$table} WHERE visited_at >= DATE_SUB(NOW(), INTERVAL %d DAY) GROUP BY url ORDER BY hits DESC LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $days, $limit ) ); } }