HEX
Server: LiteSpeed
System: Linux premium263.web-hosting.com 4.18.0-553.50.1.lve.el8.x86_64 #1 SMP Thu Apr 17 19:10:24 UTC 2025 x86_64
User: eastcjee (525)
PHP: 8.2.31
Disabled: NONE
Upload Files
File: //proc/self/cwd/wp-content/plugins/burst-statistics/includes/Frontend/class-frontend-statistics.php
<?php
namespace Burst\Frontend;

use Burst\Admin\Statistics\Statistics_Query;
use Burst\Traits\Helper;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class Frontend_Statistics
 *
 * This class handles statistics queries specifically for frontend use cases,
 * such as shortcodes and widgets. It provides a simplified interface to query
 * statistics without dependencies on admin functionality.
 *
 * @package Burst\Frontend
 * @since 2.1.0
 */
class Frontend_Statistics {
	use Helper;

	/**
	 * Constructor to initialize class properties
	 */
	public function __construct() {}

	/**
	 * Get date range based on period.
	 *
	 * @param string $period     The period ('today', '7days', '30days', etc.).
	 * @param string $start_date Custom start date (Y-m-d format).
	 * @param string $end_date   Custom end date (Y-m-d format).
	 * @return array{start: int, end: int} Array with start and end timestamps.
	 */
	public function get_date_range( string $period, string $start_date = '', string $end_date = '' ): array {
		// Handle custom date ranges first.
		if ( ! empty( $start_date ) && ! empty( $end_date ) ) {
			// Use convert_date_to_unix for proper timezone handling.
			$start = self::convert_date_to_unix( $start_date . ' 00:00:00' );
			$end   = self::convert_date_to_unix( $end_date . ' 23:59:59' );
			return [
				'start' => $start,
				'end'   => $end,
			];
		}

		// Get current time with proper timezone handling.
		$now = time();

		// Process predefined periods with timezone-aware calculations and normalization.
		switch ( $period ) {
			case 'today':
				$start = self::convert_date_to_unix( gmdate( 'Y-m-d' ) . ' 00:00:00' );
				// Normalize to nearest hour for consistent caching.
				$end = (int) ( floor( $now / HOUR_IN_SECONDS ) * HOUR_IN_SECONDS );
				break;
			case 'yesterday':
				$yesterday = gmdate( 'Y-m-d', strtotime( '-1 day' ) );
				$start     = self::convert_date_to_unix( $yesterday . ' 00:00:00' );
				$end       = self::convert_date_to_unix( $yesterday . ' 23:59:59' );
				break;
			case '7days':
				$start_date = gmdate( 'Y-m-d', strtotime( '-7 days' ) );
				$start      = self::convert_date_to_unix( $start_date . ' 00:00:00' );
				// Normalize to nearest 6 hours for consistent caching.
				$end = (int) ( floor( $now / ( 6 * HOUR_IN_SECONDS ) ) * ( 6 * HOUR_IN_SECONDS ) );
				break;
			case '14days':
				$start_date = gmdate( 'Y-m-d', strtotime( '-14 days' ) );
				$start      = self::convert_date_to_unix( $start_date . ' 00:00:00' );
				// Normalize to nearest 6 hours for consistent caching.
				$end = (int) ( floor( $now / ( 6 * HOUR_IN_SECONDS ) ) * ( 6 * HOUR_IN_SECONDS ) );
				break;
			case '30days':
				$start_date = gmdate( 'Y-m-d', strtotime( '-30 days' ) );
				$start      = self::convert_date_to_unix( $start_date . ' 00:00:00' );
				// Normalize to nearest 6 hours for consistent caching.
				$end = (int) ( floor( $now / ( 6 * HOUR_IN_SECONDS ) ) * ( 6 * HOUR_IN_SECONDS ) );
				break;
			case '90days':
				$start_date = gmdate( 'Y-m-d', strtotime( '-90 days' ) );
				$start      = self::convert_date_to_unix( $start_date . ' 00:00:00' );
				// Normalize to nearest day for consistent caching.
				$end = (int) ( floor( $now / DAY_IN_SECONDS ) * DAY_IN_SECONDS );
				break;
			case 'this_week':
				$monday = gmdate( 'Y-m-d', strtotime( 'monday this week' ) );
				$start  = self::convert_date_to_unix( $monday . ' 00:00:00' );
				// Normalize to nearest 6 hours for consistent caching.
				$end = (int) ( floor( $now / ( 6 * HOUR_IN_SECONDS ) ) * ( 6 * HOUR_IN_SECONDS ) );
				break;
			case 'last_week':
				$monday_last = gmdate( 'Y-m-d', strtotime( 'monday last week' ) );
				$sunday_last = gmdate( 'Y-m-d', strtotime( 'sunday last week' ) );
				$start       = self::convert_date_to_unix( $monday_last . ' 00:00:00' );
				$end         = self::convert_date_to_unix( $sunday_last . ' 23:59:59' );
				break;
			case 'this_month':
				$first_day = gmdate( 'Y-m-01' );
				$start     = self::convert_date_to_unix( $first_day . ' 00:00:00' );
				// Normalize to nearest day for consistent caching.
				$end = (int) ( floor( $now / DAY_IN_SECONDS ) * DAY_IN_SECONDS );
				break;
			case 'last_month':
				$first_day_last = gmdate( 'Y-m-01', strtotime( 'first day of last month' ) );
				$last_day_last  = gmdate( 'Y-m-t', strtotime( 'last day of last month' ) );
				$start          = self::convert_date_to_unix( $first_day_last . ' 00:00:00' );
				$end            = self::convert_date_to_unix( $last_day_last . ' 23:59:59' );
				break;
			case 'this_year':
				$first_day = gmdate( 'Y-01-01' );
				$start     = self::convert_date_to_unix( $first_day . ' 00:00:00' );
				// Normalize to nearest day for consistent caching.
				$end = (int) ( floor( $now / DAY_IN_SECONDS ) * DAY_IN_SECONDS );
				break;
			case 'last_year':
				$first_day_last = gmdate( 'Y-01-01', strtotime( 'first day of last year' ) );
				$last_day_last  = gmdate( 'Y-12-31', strtotime( 'last day of last year' ) );
				$start          = self::convert_date_to_unix( $first_day_last . ' 00:00:00' );
				$end            = self::convert_date_to_unix( $last_day_last . ' 23:59:59' );
				break;
			case 'all_time':
			default:
				$start = 0;
				// Normalize to nearest day for consistent caching.
				$end = (int) ( floor( $now / DAY_IN_SECONDS ) * DAY_IN_SECONDS );
				break;
		}

		return [
			'start' => $start,
			'end'   => $end,
		];
	}

	/**
	 * Generate a SQL query for frontend statistics without admin dependencies
	 *
	 * @param Statistics_Query $query_data The Query Data object.
	 * @return string SQL query.
	 */
	public function generate_statistics_query( Statistics_Query $query_data ): string {
		global $wpdb;

		if ( empty( $query_data->get_select() ) ) {
			$query_data->select( [ 'pageviews' ] );
		}

		$select       = $query_data->get_select();
		$filters      = $query_data->get_filters();
		$select_sql   = $this->build_select_metrics( $select, $query_data );
		$table_name   = $wpdb->prefix . 'burst_statistics AS statistics';
		$where        = $this->build_where_clause( $filters, $query_data );
		$group_by     = $query_data->get_group_by();
		$order_by     = $query_data->get_order_by_value();
		$group_by_sql = ! empty( $group_by ) ? 'GROUP BY ' . implode( ',', $group_by ) : '';
		$order_by_sql = ! empty( $order_by ) ? 'ORDER BY ' . implode( ',', $order_by ) : '';

		// Check if sessions join is needed based on select metrics or filters.
		$needs_sessions  = false;
		$session_metrics = [ 'bounce_rate', 'first_time_visitors', 'device', 'referrer' ];
		foreach ( $select as $metric ) {
			if ( in_array( $metric, $session_metrics, true ) ) {
				$needs_sessions = true;
				break;
			}
		}
		if ( ! $needs_sessions ) {
			$session_filter_keys = [ 'referrer', 'device', 'browser', 'platform', 'top_referrers' ];
			foreach ( array_keys( $filters ) as $filter_key ) {
				if ( in_array( $filter_key, $session_filter_keys, true ) ) {
					$needs_sessions = true;
					break;
				}
			}
		}

		$join_sql = $needs_sessions
			? "INNER JOIN {$wpdb->prefix}burst_sessions AS sessions ON statistics.session_id = sessions.ID"
			: '';

		$sql_parts = [
			"SELECT {$select_sql}",
			"FROM {$table_name}",
			$join_sql,
			'WHERE statistics.time > %d AND statistics.time < %d',
		];

		// Filter out empty parts.
		$sql_parts = array_filter( $sql_parts, fn( $part ) => ! empty( $part ) );

		if ( ! empty( $where ) ) {
			$sql_parts[] = $where;
		}

		if ( ! empty( $group_by_sql ) ) {
			$sql_parts[] = $group_by_sql;
		}

		if ( ! empty( $order_by_sql ) ) {
			$sql_parts[] = $order_by_sql;
		}

		$limit = $query_data->get_limit();
		if ( $limit > 0 ) {
			$sql_parts[] = 'LIMIT %d';
			$sql_string  = implode( ' ', $sql_parts );
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$sql = $wpdb->prepare( $sql_string, $query_data->get_date_start(), $query_data->get_date_end(), $limit );
		} else {
			$sql_string = implode( ' ', $sql_parts );
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$sql = $wpdb->prepare( $sql_string, $query_data->get_date_start(), $query_data->get_date_end() );
		}

		return $sql;
	}

	/**
	 * Get lookup table ID for a given item and name
	 *
	 * @param string $item The item type (device, browser, platform).
	 * @param string $name The name to look up.
	 * @return int The ID from the lookup table, or 0 if not found.
	 */
	private function get_lookup_table_id( string $item, string $name ): int {
		// Validate item type.
		$allowed_items = [ 'device', 'browser', 'platform' ];
		if ( ! in_array( $item, $allowed_items, true ) ) {
			return 0;
		}

		// Try to get from cache first.
		$cache_key = 'burst_' . $item . '_name_' . crc32( $name );
		$id        = wp_cache_get( $cache_key, 'burst' );

		if ( false === $id ) {
			global $wpdb;
			$table_name = $wpdb->prefix . 'burst_' . $item . 's';

			// Execute query with error handling.
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name built from predefined array.
			$id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$table_name} WHERE name = %s LIMIT 1", $name ) );

			// Check for database errors.
			if ( $wpdb->last_error ) {
				// Log the error for debugging.
				self::error_log( 'DB Error in get_lookup_table_id(): ' . $wpdb->last_error );
				// Return safe default.
				return 0;
			}

			$id = $id ? (int) $id : 0;

			// Cache the result.
			wp_cache_set( $cache_key, $id, 'burst' );
		}

		return (int) $id;
	}

	/**
	 * Get lookup table name by ID
	 *
	 * @param string $item The item type (device, browser, platform).
	 * @param int    $id   The ID to look up.
	 * @return string The name from the lookup table, or empty string if not found.
	 */
	private function get_lookup_table_name_by_id( string $item, int $id ): string {
		if ( $id === 0 ) {
			return '';
		}

		// Validate item type.
		$allowed_items = [ 'device', 'browser', 'platform' ];
		if ( ! in_array( $item, $allowed_items, true ) ) {
			return '';
		}

		// Try to get from cache first.
		$cache_key = 'burst_' . $item . '_' . $id;
		$name      = wp_cache_get( $cache_key, 'burst' );

		if ( false === $name ) {
			global $wpdb;
			$table_name = $wpdb->prefix . 'burst_' . $item . 's';

			// Execute query with error handling.
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name built from predefined array.
			$name = $wpdb->get_var( $wpdb->prepare( "SELECT name FROM {$table_name} WHERE ID = %s LIMIT 1", $id ) );

			// Check for database errors.
			if ( $wpdb->last_error ) {
				// Log the error for debugging.
				self::error_log( 'DB Error in get_lookup_table_name_by_id(): ' . $wpdb->last_error );
				// Return safe default.
				return '';
			}

			$name = $name ? (string) $name : '';

			// Cache the result.
			wp_cache_set( $cache_key, $name, 'burst' );
		}

		return (string) $name;
	}

	/**
	 * Get device name by ID (public method for shortcode usage)
	 *
	 * @param int $device_id The device ID.
	 * @return string The device name.
	 */
	public function get_device_name_by_id( int $device_id ): string {
		return $this->get_lookup_table_name_by_id( 'device', $device_id );
	}

	/**
	 * Build the SELECT clause for chosen metrics
	 *
	 * @param array            $metrics Metrics to include.
	 * @param Statistics_Query $query_data The parameters for querying.
	 * @return string SELECT clause.
	 */
	private function build_select_metrics( array $metrics, Statistics_Query $query_data ): string {
		$select_parts    = [];
		$exclude_bounces = $query_data->get_exclude_bounces();

		foreach ( $metrics as $metric ) {
			if ( ! in_array( $metric, $query_data->get_allowlist()->metrics(), true ) ) {
				continue;
			}

			switch ( $metric ) {
				case 'pageviews':
					$select_parts[] = $exclude_bounces
						? 'COALESCE( SUM( CASE WHEN sessions.bounce = 0 THEN 1 ELSE 0 END ), 0) as pageviews'
						: 'COUNT(statistics.ID) as pageviews';
					break;
				case 'visitors':
					$select_parts[] = $exclude_bounces
						? 'COUNT(DISTINCT CASE WHEN sessions.bounce = 0 THEN statistics.uid END) as visitors'
						: 'COUNT(DISTINCT statistics.uid) as visitors';
					break;
				case 'sessions':
					$select_parts[] = $exclude_bounces
						? 'COUNT( DISTINCT CASE WHEN sessions.bounce = 0 THEN statistics.session_id END ) as sessions'
						: 'COUNT(DISTINCT statistics.session_id) as sessions';
					break;
				case 'bounce_rate':
					$select_parts[] = 'SUM(sessions.bounce) / COUNT(DISTINCT statistics.session_id) * 100 as bounce_rate';
					break;
				case 'avg_time_on_page':
					$select_parts[] = $exclude_bounces
						? 'COALESCE( AVG( CASE WHEN sessions.bounce = 0 THEN statistics.time_on_page END ), 0 ) as avg_time_on_page'
						: 'AVG(statistics.time_on_page) as avg_time_on_page';
					break;
				case 'first_time_visitors':
					$select_parts[] = $exclude_bounces
						? 'COALESCE( SUM( CASE WHEN sessions.bounce = 0 THEN sessions.first_time_visit ELSE 0 END ), 0 ) as first_time_visitors'
						: 'SUM(sessions.first_time_visit) as first_time_visitors';
					break;
				case 'page_url':
					$select_parts[] = 'statistics.page_url';
					break;
				case 'referrer':
					$select_parts[] = 'sessions.referrer';
					break;
				case 'device':
					$select_parts[] = 'sessions.device_id';
					break;
				case 'count':
				default:
					$select_parts[] = $exclude_bounces
						? 'COALESCE( SUM( CASE WHEN sessions.bounce = 0 THEN 1 ELSE 0 END ), 0) as count'
						: 'COUNT(statistics.ID) as count';
					break;
			}
		}

		return implode( ', ', $select_parts );
	}

	/**
	 * Build WHERE clause for frontend queries
	 *
	 * @param array $filters Filter conditions.
	 * @return string WHERE clause.
	 */
	private function build_where_clause( array $filters, Statistics_Query $query_data ): string {
		global $wpdb;
		$where_parts = [];

		foreach ( $filters as $key => $value ) {
			if ( ! in_array( $key, $query_data->get_allowlist()->filter_keys(), true ) ) {
				continue;
			}

			switch ( $key ) {
				case 'page_id':
					$where_parts[] = $wpdb->prepare( 'statistics.page_id = %s', $value );
					break;
				case 'page_type':
					$where_parts[] = $wpdb->prepare( 'statistics.page_type = %s', $value );
					break;
				case 'page_url':
					$where_parts[] = $wpdb->prepare( 'statistics.page_url = %s', $value );
					break;
				case 'referrer':
					if ( $value === 'Direct' || $value === __( 'Direct', 'burst-statistics' ) ) {
						$where_parts[] = "(sessions.referrer = '' OR sessions.referrer IS NULL)";
					} else {
						$where_parts[] = $wpdb->prepare( 'sessions.referrer LIKE %s', '%' . $wpdb->esc_like( $value ) . '%' );
					}
					break;
				case 'device':
					$device_id     = $this->get_lookup_table_id( 'device', $value );
					$where_parts[] = $wpdb->prepare( 'sessions.device_id = %d', $device_id );
					break;
				case 'browser':
					$browser_id    = $this->get_lookup_table_id( 'browser', $value );
					$where_parts[] = $wpdb->prepare( 'sessions.browser_id = %d', $browser_id );
					break;
				case 'platform':
					$platform_id   = $this->get_lookup_table_id( 'platform', $value );
					$where_parts[] = $wpdb->prepare( 'sessions.platform_id = %d', $platform_id );
					break;
				default:
					break;
			}
		}

		// Handle referrer filtering to exclude own site.
		if ( isset( $filters['referrer'] ) || isset( $filters['top_referrers'] ) ) {
			$site_url      = str_replace( [ 'http://www.', 'https://www.', 'http://', 'https://' ], '', site_url() );
			$where_parts[] = $wpdb->prepare( 'sessions.referrer NOT LIKE %s', '%' . $wpdb->esc_like( $site_url ) . '%' );
		}

		return ! empty( $where_parts ) ? 'AND ' . implode( ' AND ', $where_parts ) : '';
	}

	/**
	 * Get most viewed posts
	 *
	 * @param int    $count Number of posts to retrieve.
	 * @param string $post_type Post type to query.
	 * @param int    $start_time Start timestamp (default: 0 for all time).
	 * @param int    $end_time End timestamp (default: current time).
	 * @return array<int, array<string, mixed>> Array of post objects with view counts.
	 */
	public function get_most_viewed_posts( int $count = 5, string $post_type = 'post', int $start_time = 0, int $end_time = 0 ): array {
		// Sanitize post type.
		$post_types = get_post_types();
		if ( ! in_array( $post_type, $post_types, true ) ) {
			$post_type = 'post';
		}

		if ( $end_time === 0 ) {
			$end_time = time();
		}

		global $wpdb;
		// Get posts sorted by pageviews.
		$posts  = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT page_id, COUNT(*) as pageview_count
             FROM {$wpdb->prefix}burst_statistics
             WHERE page_id > 0
               AND time >= %d
               AND time <= %d
               AND page_type = %s
             GROUP BY page_id
             ORDER BY pageview_count DESC
             LIMIT %d",
				$start_time,
				$end_time,
				$post_type,
				$count
			),
			ARRAY_A
		);
		$result = [];

		foreach ( $posts as $post ) {
			$post_object = get_post( $post['page_id'] );
			$result[]    = [
				'post'  => $post_object,
				'views' => $post['pageview_count'],
			];
		}

		return $result;
	}
}