repo = new ViewsRepository(); $this->bot_filter = new BotFilter(); $this->sessions = new SessionManager(); } public function init(): void { $mode = $this->get_tracking_mode(); match ( $mode ) { 'php' => add_action( 'template_redirect', [ $this, 'maybe_track_php' ], 20 ), 'js' => add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_js_tracker' ] ), 'both' => ( function () { add_action( 'template_redirect', [ $this, 'maybe_track_php' ], 20 ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_js_tracker' ] ); } )(), default => null, }; } public function maybe_track_php(): void { if ( ! $this->should_track() ) { return; } $post_id = get_queried_object_id(); if ( ! $post_id ) { return; } $this->record_view( $post_id ); } public function record_view( int $post_id, array $context = [] ): bool { if ( ! $this->is_trackable_post( $post_id ) ) { return false; } $interval_minutes = (int) $this->get_setting( 'session_interval', 30 ); $interval_seconds = $interval_minutes * MINUTE_IN_SECONDS; $ip_hash = $this->hash_ip( $this->get_real_ip() ); $session_key = $this->sessions->get_or_create( $interval_seconds ); $user_agent = sanitize_text_field( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ) ); $referrer = sanitize_url( wp_get_referer() ?: '' ); $is_bot = $this->bot_filter->is_bot( $user_agent ); $should = apply_filters( 'pulseviews/should_track', true, $post_id, $context ); if ( ! $should ) { return false; } // If interval is greater than 0, check duplicate records. If 0, bypass duplicate check. if ( $interval_seconds > 0 && $this->repo->is_duplicate( $post_id, $session_key, $ip_hash, $interval_seconds ) ) { return false; } if ( $this->is_excluded( $ip_hash ) ) { return false; } $real_ip = $this->get_real_ip(); $country_code = $context['country_code'] ?? $this->resolve_country( $real_ip ); $view_id = $this->repo->insert_view( [ 'post_id' => $post_id, 'ip_hash' => $ip_hash, 'session_key' => $session_key, 'user_agent' => $user_agent, 'is_bot' => $is_bot, 'country_code' => $country_code, 'referrer' => $referrer, ] ); if ( $view_id > 0 ) { wp_cache_delete( "pulseviews_totals_{$post_id}_total", 'pulseviews' ); wp_cache_delete( "pulseviews_totals_{$post_id}_today", 'pulseviews' ); wp_cache_delete( 'pulseviews_site_totals', 'pulseviews' ); do_action( 'pulseviews/view_recorded', $view_id, $post_id, $context ); return true; } return false; } public function enqueue_js_tracker(): void { if ( ! is_singular() ) { return; } wp_enqueue_script( 'pulseviews-tracker', PULSEVIEWS_URL . 'assets/frontend/tracker.js', [], PULSEVIEWS_VERSION, [ 'strategy' => 'defer', 'in_footer' => true ] ); wp_localize_script( 'pulseviews-tracker', 'pulseviewsTracker', [ 'endpoint' => rest_url( 'pulseviews/v1/track' ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'postId' => get_queried_object_id(), 'mode' => $this->get_tracking_mode(), ] ); } private function should_track(): bool { if ( ! is_singular() ) { return false; } if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return false; } if ( wp_doing_ajax() || wp_doing_cron() || is_admin() ) { return false; } if ( $this->get_setting( 'exclude_logged_in', false ) && is_user_logged_in() ) { return false; } $excluded_roles = (array) $this->get_setting( 'exclude_roles', [ 'administrator', 'editor' ] ); if ( is_user_logged_in() && $this->current_user_has_role( $excluded_roles ) ) { return false; } return true; } private function is_trackable_post( int $post_id ): bool { $post = get_post( $post_id ); if ( ! $post || 'publish' !== $post->post_status ) { return false; } $allowed_types = (array) $this->get_setting( 'post_types', [ 'post', 'page' ] ); $allowed_types = apply_filters( 'pulseviews/tracked_post_types', $allowed_types, $post_id ); return in_array( $post->post_type, $allowed_types, true ); } private function is_excluded( string $ip_hash ): bool { $exclusions = wp_cache_get( 'pulseviews_exclusions', 'pulseviews' ); if ( false === $exclusions ) { global $wpdb; $table = $wpdb->prefix . 'pulseviews_exclusions'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $exclusions = $wpdb->get_results( $wpdb->prepare( 'SELECT type, value FROM %i', $table ), ARRAY_A ); wp_cache_set( 'pulseviews_exclusions', $exclusions, 'pulseviews', 300 ); } foreach ( (array) $exclusions as $rule ) { if ( 'ip' === $rule['type'] && $rule['value'] === $ip_hash ) { return true; } } return false; } private function resolve_country( string $ip ): string { $cf = sanitize_text_field( sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_IPCOUNTRY'] ?? '' ) ) ); if ( $cf && 'XX' !== $cf && 2 === strlen( $cf ) ) { return strtoupper( $cf ); } foreach ( [ 'HTTP_X_COUNTRY_CODE', 'HTTP_X_GEO_COUNTRY', 'GEOIP_COUNTRY_CODE' ] as $h ) { $val = sanitize_text_field( wp_unslash( $_SERVER[ $h ] ?? '' ) ); if ( $val && 2 === strlen( $val ) ) { return strtoupper( $val ); } } if ( ! $this->get_setting( 'geo_tracking', false ) ) { return ''; } if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { return ''; } $cache_key = 'pulseviews_geo_' . substr( md5( $ip ), 0, 16 ); $cached = get_transient( $cache_key ); if ( false !== $cached ) { return (string) $cached; } $response = wp_remote_get( 'http://ip-api.com/json/' . rawurlencode( $ip ) . '?fields=countryCode,status', [ 'timeout' => 2, 'user-agent' => 'PulseViews/' . PULSEVIEWS_VERSION . ' (WordPress)', ] ); $country = ''; if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) { $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( is_array( $data ) && 'success' === ( $data['status'] ?? '' ) && isset( $data['countryCode'] ) ) { $country = strtoupper( sanitize_text_field( $data['countryCode'] ) ); } } set_transient( $cache_key, $country, DAY_IN_SECONDS ); return $country; } private function get_real_ip(): string { $trust_proxy = $this->get_setting( 'trust_proxy_headers', false ); if ( $trust_proxy ) { $headers = [ 'HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR', ]; foreach ( $headers as $header ) { if ( ! empty( $_SERVER[ $header ] ) ) { $ip = trim( explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $header ] ?? '' ) ) )[0] ); if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) { return $ip; } } } } return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0' ) ); } private function hash_ip( string $ip ): string { return hash( 'sha256', $ip . wp_salt( 'auth' ) ); } private function get_tracking_mode(): string { return (string) $this->get_setting( 'tracking_mode', 'php' ); } private function current_user_has_role( array $roles ): bool { $user = wp_get_current_user(); return (bool) array_intersect( $roles, (array) $user->roles ); } private function get_setting( string $key, mixed $default = null ): mixed { static $settings = null; if ( null === $settings ) { $settings = get_option( 'pulseviews_settings', [] ); } return $settings[ $key ] ?? $default; } }