get_charset_collate(); $daily_table = self::daily_table_name(); $post_daily_table = self::post_daily_table_name(); $page_daily_table = self::page_daily_table_name(); $referral_table = self::referral_table_name(); $dimension_table = self::dimension_table_name(); $sql_daily = "CREATE TABLE {$daily_table} ( stat_date date NOT NULL, views bigint(20) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (stat_date) ) {$charset_collate};"; $sql_post_daily = "CREATE TABLE {$post_daily_table} ( post_id bigint(20) unsigned NOT NULL, stat_date date NOT NULL, views bigint(20) unsigned NOT NULL DEFAULT 0, unique_visitors bigint(20) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (post_id, stat_date), KEY stat_date (stat_date), KEY views (views) ) {$charset_collate};"; $sql_page_daily = "CREATE TABLE {$page_daily_table} ( page_key varchar(191) NOT NULL, page_type varchar(32) NOT NULL, object_id bigint(20) unsigned NULL DEFAULT NULL, page_label varchar(255) NOT NULL, page_url text NULL, stat_date date NOT NULL, views bigint(20) unsigned NOT NULL DEFAULT 0, unique_visitors bigint(20) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (page_key, stat_date), KEY stat_date (stat_date), KEY page_type (page_type), KEY object_id (object_id), KEY views (views) ) {$charset_collate};"; $sql_referral = "CREATE TABLE {$referral_table} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, stat_date date NOT NULL, category varchar(32) NOT NULL, source_url text NULL, source_host varchar(191) NULL, visits bigint(20) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY unique_referral (stat_date, category, source_host(191), source_url(191)), KEY stat_date (stat_date), KEY category (category), KEY source_host (source_host) ) {$charset_collate};"; $sql_dimensions = "CREATE TABLE {$dimension_table} ( stat_date date NOT NULL, dimension_type varchar(32) NOT NULL, dimension_value varchar(191) NOT NULL, views bigint(20) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (stat_date, dimension_type, dimension_value), KEY dimension_type (dimension_type), KEY views (views) ) {$charset_collate};"; dbDelta($sql_daily); dbDelta($sql_post_daily); dbDelta($sql_page_daily); dbDelta($sql_referral); dbDelta($sql_dimensions); if (!get_option(self::INSTALL_OPTION_KEY)) { update_option(self::INSTALL_OPTION_KEY, current_time('Y-m-d'), false); } update_option(self::SCHEMA_VERSION_OPTION_KEY, self::VERSION, false); } public static function deactivate() { // Keep data on deactivation. } public function maybe_upgrade_schema() { $stored_version = (string) get_option(self::SCHEMA_VERSION_OPTION_KEY, ''); if ($stored_version === self::VERSION) { return; } self::activate(); } public function load_textdomain() { load_plugin_textdomain('ansico-stat-plugin', false, dirname(plugin_basename(__FILE__)) . '/languages'); } public static function daily_table_name() { global $wpdb; return $wpdb->prefix . 'ansico_stat_daily'; } public static function post_daily_table_name() { global $wpdb; return $wpdb->prefix . 'ansico_stat_post_daily'; } public static function page_daily_table_name() { global $wpdb; return $wpdb->prefix . 'ansico_stat_page_daily'; } public static function referral_table_name() { global $wpdb; return $wpdb->prefix . 'ansico_stat_referrals'; } public static function dimension_table_name() { global $wpdb; return $wpdb->prefix . 'ansico_stat_dimensions'; } protected function get_default_track_post_types() { $post_types = get_post_types(['public' => true], 'names'); $post_types = array_filter($post_types, function($post_type) { return !in_array($post_type, ['attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block', 'wp_template', 'wp_template_part', 'wp_navigation', 'wp_global_styles'], true); }); return array_values($post_types); } protected function get_settings() { $settings = get_option(self::OPTION_KEY, []); if (!is_array($settings)) { $settings = []; } $settings = wp_parse_args($settings, [ 'visitor_counting_mode' => 'count_all', 'exclude_known_bots' => 1, 'top_list_rows' => 10, 'referral_rows' => 25, 'revisit_minutes' => 30, 'frontend_label' => 'Views', 'track_post_types' => $this->get_default_track_post_types(), 'track_front_page' => 1, 'track_posts_page' => 1, 'track_archives' => 1, 'track_search' => 1, 'track_404' => 1, ]); if (!is_array($settings['track_post_types'])) { $settings['track_post_types'] = $this->get_default_track_post_types(); } $settings['track_post_types'] = array_values(array_filter(array_map('sanitize_key', $settings['track_post_types']))); $settings['visitor_counting_mode'] = in_array($settings['visitor_counting_mode'], ['count_all', 'exclude_admins', 'exclude_all_logged_in'], true) ? $settings['visitor_counting_mode'] : 'count_all'; $settings['revisit_minutes'] = max(1, min(10080, (int) $settings['revisit_minutes'])); $settings['frontend_label'] = sanitize_text_field((string) $settings['frontend_label']); if ($settings['frontend_label'] === '') { $settings['frontend_label'] = 'Views'; } return $settings; } protected function get_cookie_ttl() { $settings = $this->get_settings(); return max(60, (int) $settings['revisit_minutes'] * 60); } protected function should_exclude_current_user() { $settings = $this->get_settings(); $mode = $settings['visitor_counting_mode'] ?? 'count_all'; if ($mode === 'exclude_all_logged_in' && is_user_logged_in()) { return true; } if ($mode === 'exclude_admins' && current_user_can('manage_options')) { return true; } return false; } protected function is_tracking_enabled_for_context(array $page_context) { $settings = $this->get_settings(); $page_type = (string) ($page_context['page_type'] ?? ''); if (($page_context['kind'] ?? '') === 'singular') { $post_type = sanitize_key((string) ($page_context['post_type'] ?? get_post_type((int) ($page_context['post_id'] ?? 0)))); return in_array($post_type, $settings['track_post_types'], true); } if ($page_type === 'front_page') { return !empty($settings['track_front_page']); } if ($page_type === 'home') { return !empty($settings['track_posts_page']); } if ($page_type === 'search') { return !empty($settings['track_search']); } if ($page_type === '404') { return !empty($settings['track_404']); } return !empty($settings['track_archives']); } protected function get_frontend_icon_markup() { return ''; } protected function get_country_code() { $candidates = [ $_SERVER['HTTP_CF_IPCOUNTRY'] ?? '', $_SERVER['GEOIP_COUNTRY_CODE'] ?? '', $_SERVER['HTTP_X_COUNTRY_CODE'] ?? '', $_SERVER['HTTP_X_APPENGINE_COUNTRY'] ?? '', ]; foreach ($candidates as $candidate) { $code = strtoupper(sanitize_text_field((string) $candidate)); if (preg_match('/^[A-Z]{2}$/', $code)) { return $code; } } return 'Unknown'; } protected function get_country_label(string $country_code) { $country_code = strtoupper(trim($country_code)); if ($country_code === '' || $country_code === 'UNKNOWN') { return __('Unknown', 'ansico-stat-plugin'); } if (function_exists('locale_get_display_region')) { $label = locale_get_display_region('-' . $country_code, get_locale()); if (is_string($label) && $label !== '') { return $label; } } return $country_code; } protected function get_device_type() { $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; if ($user_agent === '') { return 'Unknown'; } if (strpos($user_agent, 'tablet') !== false || strpos($user_agent, 'ipad') !== false || strpos($user_agent, 'kindle') !== false || strpos($user_agent, 'playbook') !== false) { return 'Tablet'; } if (strpos($user_agent, 'mobile') !== false || strpos($user_agent, 'iphone') !== false || strpos($user_agent, 'android') !== false || strpos($user_agent, 'phone') !== false) { return 'Mobile'; } return 'Desktop'; } protected function get_browser_name() { $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; $browsers = [ 'edg/' => 'Edge', 'opr/' => 'Opera', 'opera' => 'Opera', 'brave' => 'Brave', 'firefox' => 'Firefox', 'samsungbrowser' => 'Samsung Internet', 'chrome' => 'Chrome', 'crios' => 'Chrome', 'safari' => 'Safari', 'trident/' => 'Internet Explorer', 'msie' => 'Internet Explorer', ]; foreach ($browsers as $needle => $label) { if (strpos($user_agent, $needle) !== false) { if ($label === 'Safari' && (strpos($user_agent, 'chrome') !== false || strpos($user_agent, 'crios') !== false || strpos($user_agent, 'android') !== false)) { continue; } return $label; } } return 'Other'; } protected function get_os_name() { $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; $systems = [ 'windows nt' => 'Windows', 'iphone' => 'iOS', 'ipad' => 'iPadOS', 'mac os x' => 'macOS', 'android' => 'Android', 'linux' => 'Linux', 'cros' => 'Chrome OS', ]; foreach ($systems as $needle => $label) { if (strpos($user_agent, $needle) !== false) { return $label; } } return 'Other'; } protected function increment_dimension_views(string $today) { global $wpdb; $table = self::dimension_table_name(); $dimensions = [ 'country' => $this->get_country_code(), 'device' => $this->get_device_type(), 'browser' => $this->get_browser_name(), 'os' => $this->get_os_name(), ]; foreach ($dimensions as $dimension_type => $dimension_value) { $dimension_value = substr(sanitize_text_field((string) $dimension_value), 0, 191); if ($dimension_value === '') { $dimension_value = 'Unknown'; } $wpdb->query($wpdb->prepare( "INSERT INTO {$table} (stat_date, dimension_type, dimension_value, views) VALUES (%s, %s, %s, 1) ON DUPLICATE KEY UPDATE views = views + 1", $today, $dimension_type, $dimension_value )); } } protected function is_known_bot_request() { $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : ''; if ($user_agent === '') { return true; } $bot_markers = [ 'bot', 'crawl', 'crawler', 'spider', 'slurp', 'facebookexternalhit', 'bingpreview', 'headless', 'preview', 'python-requests', 'curl', 'wget', 'uptimerobot', 'monitoring', 'phantomjs', 'feedfetcher', 'google-read-aloud', 'scanner', 'validator', 'scrapy', 'axios', 'go-http-client', 'node-fetch', 'httpclient', 'libwww-perl' ]; foreach ($bot_markers as $marker) { if (strpos($user_agent, $marker) !== false) { return true; } } if (!empty($_SERVER['HTTP_X_PURPOSE']) || !empty($_SERVER['HTTP_X_MOZ']) || (!empty($_SERVER['HTTP_SEC_FETCH_SITE']) && strtolower((string) $_SERVER['HTTP_SEC_FETCH_SITE']) === 'none' && strpos($user_agent, 'mozilla') === false)) { return true; } return false; } protected function is_trackable_post($post) { if (!$post || empty($post->ID) || !is_object($post)) { return false; } $post_type_obj = get_post_type_object($post->post_type); if (!$post_type_obj || empty($post_type_obj->public)) { return false; } if (post_password_required($post)) { return false; } return true; } protected function get_referer_data() { $referer = isset($_SERVER['HTTP_REFERER']) ? trim((string) wp_unslash($_SERVER['HTTP_REFERER'])) : ''; $site_host = wp_parse_url(home_url(), PHP_URL_HOST); $site_host = $site_host ? strtolower((string) $site_host) : ''; if ($referer === '') { return [ 'category' => 'direct', 'source_url' => '', 'source_host'=> '', ]; } $host = wp_parse_url($referer, PHP_URL_HOST); $host = $host ? strtolower((string) $host) : ''; if ($host === '' || $host === $site_host || preg_replace('/^www\./', '', $host) === preg_replace('/^www\./', '', $site_host)) { return [ 'category' => 'direct', 'source_url' => '', 'source_host'=> '', ]; } $search_hosts = [ 'google.', 'bing.com', 'search.yahoo.', 'duckduckgo.com', 'yandex.', 'baidu.com', 'ecosia.org', 'startpage.com', 'qwant.com', 'search.brave.com' ]; foreach ($search_hosts as $needle) { if (strpos($host, $needle) !== false) { return [ 'category' => 'search', 'source_url' => esc_url_raw($referer), 'source_host'=> sanitize_text_field($host), ]; } } $social_hosts = [ 'facebook.com', 'm.facebook.com', 'l.facebook.com', 'lm.facebook.com', 'instagram.com', 't.co', 'twitter.com', 'x.com', 'linkedin.com', 'lnkd.in', 'pinterest.', 'reddit.com', 'youtube.com', 'youtu.be', 'tiktok.com', 'snapchat.com', 'threads.net', 'messenger.com' ]; foreach ($social_hosts as $needle) { if (strpos($host, $needle) !== false) { return [ 'category' => 'social', 'source_url' => esc_url_raw($referer), 'source_host'=> sanitize_text_field($host), ]; } } return [ 'category' => 'website', 'source_url' => esc_url_raw($referer), 'source_host'=> sanitize_text_field($host), ]; } public function maybe_track_view() { if (is_admin() || wp_doing_ajax() || wp_doing_cron() || is_feed() || is_preview() || is_trackback() || is_robots()) { return; } if (!$this->is_trackable_request()) { return; } $settings = $this->get_settings(); if ($this->should_exclude_current_user()) { return; } if (!empty($settings['exclude_known_bots']) && $this->is_known_bot_request()) { return; } $page_context = $this->get_current_page_context(); if (!$page_context || !$this->is_tracking_enabled_for_context($page_context)) { return; } $view_cookie_name = self::COOKIE_PREFIX . md5($page_context['cookie_key']); $unique_cookie_name = self::COOKIE_PREFIX . 'unique_' . md5($page_context['cookie_key'] . '|' . current_time('Y-m-d')); $is_unique_visitor = !isset($_COOKIE[$unique_cookie_name]); if (isset($_COOKIE[$view_cookie_name])) { return; } $referer_data = $this->get_referer_data(); if ($page_context['kind'] === 'singular') { $this->increment_post_views((int) $page_context['post_id'], $referer_data, $is_unique_visitor); } else { $this->increment_non_singular_views($page_context, $referer_data, $is_unique_visitor); } if (!headers_sent()) { setcookie($view_cookie_name, '1', time() + $this->get_cookie_ttl(), COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true); $_COOKIE[$view_cookie_name] = '1'; if ($is_unique_visitor) { $expires = strtotime('tomorrow', current_time('timestamp')); if ($expires <= time()) { $expires = time() + DAY_IN_SECONDS; } setcookie($unique_cookie_name, '1', $expires, COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true); $_COOKIE[$unique_cookie_name] = '1'; } } } protected function is_trackable_request() { return is_singular() || is_404() || is_home() || is_front_page() || is_post_type_archive() || is_category() || is_tag() || is_tax() || is_author() || is_date() || is_search() || is_archive(); } protected function normalize_request_uri() { $uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '/'; $path = wp_parse_url($uri, PHP_URL_PATH); $path = is_string($path) && $path !== '' ? $path : '/'; return untrailingslashit($path) ?: '/'; } protected function get_current_page_context() { if (is_admin()) { return null; } if (is_singular()) { $post = $this->get_current_singular_post(); if (!$post) { return null; } return [ 'kind' => 'singular', 'post_id' => (int) $post->ID, 'post_type' => sanitize_key((string) $post->post_type), 'cookie_key' => 'post:' . (int) $post->ID, 'page_key' => 'post:' . (int) $post->ID, 'page_type' => sanitize_key((string) $post->post_type), 'object_id' => (int) $post->ID, 'label' => get_the_title($post->ID) ?: __('(no title)', 'ansico-stat-plugin'), 'url' => get_permalink($post->ID) ?: '', ]; } $request_path = $this->normalize_request_uri(); $base = [ 'kind' => 'archive', 'cookie_key' => $request_path, 'page_key' => '', 'page_type' => 'archive', 'object_id' => 0, 'label' => '', 'url' => home_url($request_path . '/'), ]; if (is_404()) { $base['page_type'] = '404'; $base['page_key'] = '404:' . md5($request_path); $base['label'] = sprintf(__('404: %s', 'ansico-stat-plugin'), $request_path); $base['url'] = home_url($request_path === '/' ? '/' : $request_path . '/'); return $base; } if (is_category() || is_tag() || is_tax()) { $term = get_queried_object(); if (!$term || empty($term->term_id)) { return null; } $taxonomy = is_string($term->taxonomy ?? '') ? $term->taxonomy : 'taxonomy'; $base['page_type'] = $taxonomy; $base['page_key'] = 'term:' . $taxonomy . ':' . (int) $term->term_id; $base['cookie_key'] = $base['page_key']; $base['object_id'] = (int) $term->term_id; $base['label'] = single_term_title('', false) ?: ($term->name ?? $request_path); $base['url'] = get_term_link($term); return $base; } if (is_author()) { $author = get_queried_object(); if (!$author || empty($author->ID)) { return null; } $base['page_type'] = 'author'; $base['page_key'] = 'author:' . (int) $author->ID; $base['cookie_key'] = $base['page_key']; $base['object_id'] = (int) $author->ID; $base['label'] = get_the_author_meta('display_name', (int) $author->ID) ?: $request_path; $base['url'] = get_author_posts_url((int) $author->ID); return $base; } if (is_post_type_archive()) { $post_type = get_query_var('post_type'); $post_type = is_array($post_type) ? reset($post_type) : $post_type; $post_type = sanitize_key((string) $post_type); if ($post_type === '') { $post_type = 'archive'; } $base['page_type'] = 'post_type_archive'; $base['page_key'] = 'post_type_archive:' . $post_type; $base['cookie_key'] = $base['page_key']; $base['label'] = post_type_archive_title('', false) ?: $request_path; $archive_link = get_post_type_archive_link($post_type); $base['url'] = $archive_link ?: $base['url']; return $base; } if (is_home() || is_front_page()) { $base['page_type'] = is_front_page() ? 'front_page' : 'home'; $base['page_key'] = $base['page_type']; $base['cookie_key'] = $base['page_key']; $base['label'] = is_front_page() ? __('Front page', 'ansico-stat-plugin') : __('Posts page', 'ansico-stat-plugin'); $base['url'] = home_url('/'); return $base; } if (is_date()) { $base['page_type'] = 'date'; $base['page_key'] = 'date:' . md5($request_path); $base['label'] = wp_get_document_title() ?: $request_path; return $base; } if (is_search()) { $search_query = get_search_query(); $base['page_type'] = 'search'; $base['page_key'] = 'search:' . md5((string) $search_query); $base['cookie_key'] = 'search:' . md5((string) $search_query); $base['label'] = sprintf(__('Search: %s', 'ansico-stat-plugin'), $search_query ?: __('(empty)', 'ansico-stat-plugin')); $base['url'] = get_search_link($search_query); return $base; } if (is_archive()) { $base['page_type'] = 'archive'; $base['page_key'] = 'archive:' . md5($request_path); $base['label'] = wp_get_document_title() ?: $request_path; return $base; } return null; } protected function increment_post_views(int $post_id, array $referer_data = [], bool $is_unique_visitor = false) { global $wpdb; $today = current_time('Y-m-d'); $daily_table = self::daily_table_name(); $post_daily_table = self::post_daily_table_name(); $page_daily_table = self::page_daily_table_name(); $referral_table = self::referral_table_name(); $current_total = (int) get_post_meta($post_id, self::TOTAL_META_KEY, true); update_post_meta($post_id, self::TOTAL_META_KEY, $current_total + 1); $wpdb->query($wpdb->prepare( "INSERT INTO {$daily_table} (stat_date, views) VALUES (%s, 1) ON DUPLICATE KEY UPDATE views = views + 1", $today )); $wpdb->query($wpdb->prepare( "INSERT INTO {$post_daily_table} (post_id, stat_date, views, unique_visitors) VALUES (%d, %s, 1, %d) ON DUPLICATE KEY UPDATE views = views + 1, unique_visitors = unique_visitors + VALUES(unique_visitors)", $post_id, $today, $is_unique_visitor ? 1 : 0 )); $category = isset($referer_data['category']) ? sanitize_key($referer_data['category']) : 'direct'; $source_url = isset($referer_data['source_url']) ? esc_url_raw((string) $referer_data['source_url']) : ''; $source_host = isset($referer_data['source_host']) ? sanitize_text_field((string) $referer_data['source_host']) : ''; $wpdb->query($wpdb->prepare( "INSERT INTO {$referral_table} (stat_date, category, source_url, source_host, visits) VALUES (%s, %s, %s, %s, 1) ON DUPLICATE KEY UPDATE visits = visits + 1", $today, $category, $source_url, $source_host )); $this->increment_dimension_views($today); } protected function increment_non_singular_views(array $page_context, array $referer_data = [], bool $is_unique_visitor = false) { global $wpdb; $today = current_time('Y-m-d'); $daily_table = self::daily_table_name(); $page_daily_table = self::page_daily_table_name(); $referral_table = self::referral_table_name(); $wpdb->query($wpdb->prepare( "INSERT INTO {$daily_table} (stat_date, views) VALUES (%s, 1) ON DUPLICATE KEY UPDATE views = views + 1", $today )); $page_key = substr(sanitize_text_field((string) $page_context['page_key']), 0, 191); $page_type = substr(sanitize_key((string) $page_context['page_type']), 0, 32); $object_id = !empty($page_context['object_id']) ? (int) $page_context['object_id'] : null; $label = sanitize_text_field((string) $page_context['label']); $url = isset($page_context['url']) ? esc_url_raw((string) $page_context['url']) : ''; $wpdb->query($wpdb->prepare( "INSERT INTO {$page_daily_table} (page_key, page_type, object_id, page_label, page_url, stat_date, views, unique_visitors) VALUES (%s, %s, %s, %s, %s, %s, 1, %d) ON DUPLICATE KEY UPDATE views = views + 1, unique_visitors = unique_visitors + VALUES(unique_visitors), page_label = VALUES(page_label), page_url = VALUES(page_url)", $page_key, $page_type, $object_id, $label, $url, $today, $is_unique_visitor ? 1 : 0 )); $category = isset($referer_data['category']) ? sanitize_key($referer_data['category']) : 'direct'; $source_url = isset($referer_data['source_url']) ? esc_url_raw((string) $referer_data['source_url']) : ''; $source_host = isset($referer_data['source_host']) ? sanitize_text_field((string) $referer_data['source_host']) : ''; $wpdb->query($wpdb->prepare( "INSERT INTO {$referral_table} (stat_date, category, source_url, source_host, visits) VALUES (%s, %s, %s, %s, 1) ON DUPLICATE KEY UPDATE visits = visits + 1", $today, $category, $source_url, $source_host )); $this->increment_dimension_views($today); } protected function get_non_singular_lifetime_views(string $page_key) { global $wpdb; $table = self::page_daily_table_name(); $views = $wpdb->get_var($wpdb->prepare( "SELECT SUM(views) FROM {$table} WHERE page_key = %s", $page_key )); return (int) $views; } protected function get_current_singular_post() { if (is_admin() || !is_singular()) { return null; } $post = get_queried_object(); if (!$this->is_trackable_post($post)) { return null; } return $post; } protected function get_admin_view_markup(array $page_context) { if (!$this->is_tracking_enabled_for_context($page_context)) { return ''; } $views = 0; if (($page_context['kind'] ?? '') === 'singular' && !empty($page_context['post_id'])) { $views = (int) get_post_meta((int) $page_context['post_id'], self::TOTAL_META_KEY, true); } elseif (!empty($page_context['page_key'])) { $views = $this->get_non_singular_lifetime_views((string) $page_context['page_key']); } $settings = $this->get_settings(); $label = sanitize_text_field((string) ($settings['frontend_label'] ?? 'Views')); if ($label === '') { $label = 'Views'; } $markup = '
'; $markup .= $this->get_frontend_icon_markup(); $markup .= '' . esc_html($label . ':') . ' ' . esc_html(number_format_i18n($views)); $markup .= '
'; return $markup; } public function append_admin_view_count_to_content($content) { if (is_admin() || !is_singular() || !in_the_loop() || !is_main_query()) { return $content; } if (!current_user_can('manage_options')) { return $content; } $page_context = $this->get_current_page_context(); if (!$page_context || ($page_context['kind'] ?? '') !== 'singular') { return $content; } $markup = $this->get_admin_view_markup($page_context); return $markup === '' ? $content : $content . $markup; } public function render_admin_view_count_fallback() { if (is_admin() || !current_user_can('manage_options') || !$this->is_trackable_request()) { return; } if (is_singular()) { return; } $page_context = $this->get_current_page_context(); if (!$page_context) { return; } $markup_value = $this->get_admin_view_markup($page_context); if ($markup_value === '') { return; } $markup = wp_json_encode($markup_value); ?> %3$s', esc_url($permalink), esc_attr($permalink), esc_html($label) ); } protected function format_external_link(string $url) { if ($url === '') { return '—'; } return sprintf( '%3$s', esc_url($url), esc_attr($url), esc_html($url) ); } public function render_dashboard_widget() { $chart_data = $this->get_daily_chart_data(30); $monthly_total = array_sum(wp_list_pluck($chart_data, 'views')); $top_posts = $this->get_top_posts_in_period($this->date_days_ago(29), current_time('Y-m-d'), 10); echo '
'; echo '

' . esc_html(sprintf(__('Total tracked views in the last 30 days: %s', 'ansico-stat-plugin'), number_format_i18n($monthly_total))) . '

'; echo '
'; echo $this->render_chart_markup($chart_data, 'ansico-stat-widget-chart', ['is_widget' => true]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '
'; echo '

' . esc_html__('Top 10 views in the last 30 days', 'ansico-stat-plugin') . '

'; if (empty($top_posts)) { echo '

' . esc_html__('No tracked views yet.', 'ansico-stat-plugin') . '

'; } else { echo '
    '; foreach ($top_posts as $row) { $post_id = (int) $row['post_id']; $title = get_the_title($post_id) ?: __('(no title)', 'ansico-stat-plugin'); echo '
  1. '; echo $this->format_post_link($post_id, $title); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo ' (' . esc_html(number_format_i18n((int) $row['period_views'])) . ')'; echo '
  2. '; } echo '
'; } echo '

' . esc_html__('Open full statistics', 'ansico-stat-plugin') . '

'; echo '
'; } protected function render_chart_markup(array $chart_data, string $chart_id, array $args = []) { $labels = []; $values = []; foreach ($chart_data as $row) { $labels[] = wp_date(get_option('date_format'), strtotime($row['date'])); $values[] = (int) ($row['views'] ?? 0); } if (empty($chart_data)) { return '

' . esc_html__('No tracked data for this period.', 'ansico-stat-plugin') . '

'; } $max = max($values ?: [0]); $sum = array_sum($values); if ($sum <= 0) { return '

' . esc_html__('No tracked data for this period.', 'ansico-stat-plugin') . '

'; } $is_widget = !empty($args['is_widget']); $height = $is_widget ? 250 : 220; $width = $is_widget ? 920 : 860; $chart_top = $is_widget ? 12 : 20; $chart_bottom = $is_widget ? 198 : 185; $chart_left = $is_widget ? 44 : 26; $chart_right = $is_widget ? 888 : 830; $count = count($values); $stepX = $count > 1 ? ($chart_right - $chart_left) / ($count - 1) : ($chart_right - $chart_left); $points = []; $area_points = []; $circles = []; foreach ($values as $index => $value) { $x = $chart_left + ($stepX * $index); $y = $chart_bottom; if ($max > 0) { $y = $chart_top + (($chart_bottom - $chart_top) * (1 - ($value / $max))); } $points[] = round($x, 2) . ',' . round($y, 2); $area_points[] = round($x, 2) . ',' . round($y, 2); $circles[] = [ 'x' => round($x, 2), 'y' => round($y, 2), 'label' => (string) $labels[$index], 'value' => (int) $value, ]; } $area_path = ''; if (!empty($area_points)) { $area_path = $chart_left . ',' . $chart_bottom . ' ' . implode(' ', $area_points) . ' ' . $chart_right . ',' . $chart_bottom; } $y_ticks = []; $tick_count = 5; for ($i = 0; $i < $tick_count; $i++) { $ratio = $tick_count > 1 ? $i / ($tick_count - 1) : 0; $value = (int) round($max * (1 - $ratio)); $y = $chart_top + (($chart_bottom - $chart_top) * $ratio); $y_ticks[] = [ 'value' => $value, 'y' => round($y, 2), ]; } ob_start(); ?>
$point) : if ($index % $label_every !== 0 && $index !== ($count - 1)) { continue; } ?>

0 && $value > 0) ? max(6, (int) round(($value / $max) * 140)) : 0; ?>
$value) { $value = (int) $value; if ($value > 0) { $filtered[(string) $label] = $value; } } if (empty($filtered)) { return '

' . esc_html__('No tracked data for this period.', 'ansico-stat-plugin') . '

'; } $total = array_sum($filtered); $palette = ['#2271b1', '#72aee6', '#135e96', '#8c8f94', '#dba617', '#00a32a', '#d63638', '#50575e']; $cx = 90; $cy = 90; $r = 72; $startAngle = -90; $index = 0; ob_start(); ?>
$value) : $angle = ($value / $total) * 360; $endAngle = $startAngle + $angle; $x1 = $cx + $r * cos(deg2rad($startAngle)); $y1 = $cy + $r * sin(deg2rad($startAngle)); $x2 = $cx + $r * cos(deg2rad($endAngle)); $y2 = $cy + $r * sin(deg2rad($endAngle)); $largeArc = $angle > 180 ? 1 : 0; $color = $palette[$index % count($palette)]; $percent = $total > 0 ? number_format_i18n(($value / $total) * 100, 1) . '%' : '0%'; ?>
$value) : $color = $palette[$index % count($palette)]; $percent = $total > 0 ? number_format_i18n(($value / $total) * 100, 1) . '%' : '0%'; ?>
( · )
date_days_ago($days - 1); $end = current_time('Y-m-d'); $rows = $wpdb->get_results($wpdb->prepare( "SELECT stat_date, views FROM {$daily_table} WHERE stat_date BETWEEN %s AND %s ORDER BY stat_date ASC", $start, $end ), ARRAY_A); $indexed = []; foreach ($rows as $row) { $indexed[$row['stat_date']] = (int) $row['views']; } $data = []; $cursor = strtotime($start); $end_ts = strtotime($end); while ($cursor <= $end_ts) { $date = gmdate('Y-m-d', $cursor); $data[] = [ 'date' => $date, 'views' => $indexed[$date] ?? 0, ]; $cursor = strtotime('+1 day', $cursor); } return $data; } protected function get_install_month_key() { $install_date = get_option(self::INSTALL_OPTION_KEY, current_time('Y-m-d')); $timestamp = strtotime($install_date ?: current_time('Y-m-d')); return wp_date('Y-m', $timestamp ?: current_time('timestamp')); } protected function get_install_year() { $install_date = get_option(self::INSTALL_OPTION_KEY, current_time('Y-m-d')); $timestamp = strtotime($install_date ?: current_time('Y-m-d')); return (int) wp_date('Y', $timestamp ?: current_time('timestamp')); } protected function get_selected_month() { $requested = isset($_GET['month']) ? sanitize_text_field(wp_unslash($_GET['month'])) : ''; if ($requested && preg_match('/^\d{4}-\d{2}$/', $requested)) { return $requested; } return wp_date('Y-m', current_time('timestamp')); } protected function get_selected_year() { $requested = isset($_GET['year']) ? absint(wp_unslash($_GET['year'])) : 0; if ($requested >= 1970 && $requested <= 9999) { return $requested; } return (int) wp_date('Y', current_time('timestamp')); } protected function render_month_navigation(string $selected_month) { $selected_ts = strtotime($selected_month . '-01'); $current_ts = strtotime(wp_date('Y-m-01', current_time('timestamp'))); $prev_month = wp_date('Y-m', strtotime('-1 month', $selected_ts)); $next_month = wp_date('Y-m', strtotime('+1 month', $selected_ts)); $base_url = admin_url('admin.php?page=' . self::MENU_SLUG_STATS); echo '
'; echo '← ' . esc_html__('Older month', 'ansico-stat-plugin') . ''; echo '' . esc_html(wp_date('F Y', $selected_ts)) . ''; if ($selected_ts < $current_ts) { echo '' . esc_html__('Newer month', 'ansico-stat-plugin') . ' →'; } else { echo ''; } echo '
'; } protected function render_year_navigation(int $selected_year) { $current_year = (int) wp_date('Y', current_time('timestamp')); $current_page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : self::MENU_SLUG_STATS; $target_slug = $current_page === self::MENU_SLUG_YEARLY ? self::MENU_SLUG_YEARLY : self::MENU_SLUG_STATS; $base_url = admin_url('admin.php?page=' . $target_slug); echo '
'; echo '← ' . esc_html__('Older year', 'ansico-stat-plugin') . ''; echo '' . esc_html((string) $selected_year) . ''; if ($selected_year < $current_year) { echo '' . esc_html__('Newer year', 'ansico-stat-plugin') . ' →'; } else { echo ''; } echo '
'; } protected function get_top_posts_in_period(string $start, string $end, int $limit = 10) { global $wpdb; $table = self::post_daily_table_name(); $results = $wpdb->get_results($wpdb->prepare( "SELECT post_id, SUM(views) AS period_views FROM {$table} WHERE stat_date BETWEEN %s AND %s GROUP BY post_id ORDER BY period_views DESC LIMIT %d", $start, $end, $limit ), ARRAY_A); return is_array($results) ? $results : []; } protected function get_monthly_top_posts(string $month_key, int $limit = 10) { $start = $month_key . '-01'; $end = wp_date('Y-m-t', strtotime($start)); return $this->get_top_posts_in_period($start, $end, $limit); } protected function get_yearly_top_posts(int $year, int $limit = 10) { $start = sprintf('%04d-01-01', $year); $end = sprintf('%04d-12-31', $year); return $this->get_top_posts_in_period($start, $end, $limit); } protected function get_latest_month_range() { return [ 'start' => wp_date('Y-m-01', current_time('timestamp')), 'end' => wp_date('Y-m-t', current_time('timestamp')), ]; } protected function get_post_views_for_latest_month(int $post_id) { global $wpdb; $range = $this->get_latest_month_range(); $table = self::post_daily_table_name(); $views = $wpdb->get_var($wpdb->prepare( "SELECT SUM(views) FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s", $post_id, $range['start'], $range['end'] )); return (int) $views; } protected function get_referral_summary_for_month(string $month_key) { global $wpdb; $table = self::referral_table_name(); $start = $month_key . '-01'; $end = wp_date('Y-m-t', strtotime($start)); $rows = $wpdb->get_results($wpdb->prepare( "SELECT category, SUM(visits) AS total_visits FROM {$table} WHERE stat_date BETWEEN %s AND %s GROUP BY category", $start, $end ), ARRAY_A); $summary = [ 'direct' => 0, 'search' => 0, 'social' => 0, 'website' => 0, ]; foreach ($rows as $row) { $category = isset($row['category']) ? (string) $row['category'] : ''; if (isset($summary[$category])) { $summary[$category] = (int) $row['total_visits']; } } return $summary; } protected function get_referral_sources_for_month(string $month_key, string $category, int $limit = 50) { global $wpdb; $table = self::referral_table_name(); $start = $month_key . '-01'; $end = wp_date('Y-m-t', strtotime($start)); $rows = $wpdb->get_results($wpdb->prepare( "SELECT source_url, source_host, SUM(visits) AS total_visits FROM {$table} WHERE stat_date BETWEEN %s AND %s AND category = %s AND source_url <> '' GROUP BY source_url, source_host ORDER BY total_visits DESC, source_host ASC LIMIT %d", $start, $end, $category, $limit ), ARRAY_A); return is_array($rows) ? $rows : []; } protected function get_top_404_pages_for_month(string $month_key, int $limit = 10) { global $wpdb; $table = self::page_daily_table_name(); $start = $month_key . '-01'; $end = wp_date('Y-m-t', strtotime($start)); $rows = $wpdb->get_results($wpdb->prepare( "SELECT page_key, page_label, page_url, SUM(views) AS period_views FROM {$table} WHERE stat_date BETWEEN %s AND %s AND page_type = %s GROUP BY page_key, page_label, page_url ORDER BY period_views DESC, page_url ASC LIMIT %d", $start, $end, '404', $limit ), ARRAY_A); return is_array($rows) ? $rows : []; } protected function get_dimension_rows_for_month(string $month_key, string $dimension_type, int $limit = 10) { global $wpdb; $table = self::dimension_table_name(); $start = $month_key . '-01'; $end = wp_date('Y-m-t', strtotime($start)); $rows = $wpdb->get_results($wpdb->prepare( "SELECT dimension_value, SUM(views) AS total_views FROM {$table} WHERE stat_date BETWEEN %s AND %s AND dimension_type = %s GROUP BY dimension_value ORDER BY total_views DESC, dimension_value ASC LIMIT %d", $start, $end, $dimension_type, $limit ), ARRAY_A); if (!is_array($rows)) { return []; } if ($dimension_type === 'country') { foreach ($rows as &$row) { $row['dimension_label'] = $this->get_country_label((string) ($row['dimension_value'] ?? '')); } unset($row); } return $rows; } protected function get_dimension_rows_for_year(int $year, string $dimension_type, int $limit = 10) { global $wpdb; $table = self::dimension_table_name(); $start = sprintf('%04d-01-01', $year); $end = sprintf('%04d-12-31', $year); $rows = $wpdb->get_results($wpdb->prepare( "SELECT dimension_value, SUM(views) AS total_views FROM {$table} WHERE stat_date BETWEEN %s AND %s AND dimension_type = %s GROUP BY dimension_value ORDER BY total_views DESC, dimension_value ASC LIMIT %d", $start, $end, $dimension_type, $limit ), ARRAY_A); if (!is_array($rows)) { return []; } if ($dimension_type === 'country') { foreach ($rows as &$row) { $row['dimension_label'] = $this->get_country_label((string) ($row['dimension_value'] ?? '')); } unset($row); } return $rows; } protected function get_referral_summary_for_year(int $year) { global $wpdb; $table = self::referral_table_name(); $start = sprintf('%04d-01-01', $year); $end = sprintf('%04d-12-31', $year); $rows = $wpdb->get_results($wpdb->prepare( "SELECT category, SUM(visits) AS total_visits FROM {$table} WHERE stat_date BETWEEN %s AND %s GROUP BY category", $start, $end ), ARRAY_A); $summary = [ 'Direct' => 0, 'Search' => 0, 'Social' => 0, 'Other websites' => 0, ]; foreach ($rows as $row) { $category = (string) ($row['category'] ?? ''); $value = (int) ($row['total_visits'] ?? 0); if ($category === 'direct') { $summary['Direct'] = $value; } elseif ($category === 'search') { $summary['Search'] = $value; } elseif ($category === 'social') { $summary['Social'] = $value; } elseif ($category === 'website') { $summary['Other websites'] = $value; } } return $summary; } protected function get_monthly_totals_for_year(int $year) { global $wpdb; $table = self::daily_table_name(); $start = sprintf('%04d-01-01', $year); $end = sprintf('%04d-12-31', $year); $rows = $wpdb->get_results($wpdb->prepare( "SELECT stat_date, views FROM {$table} WHERE stat_date BETWEEN %s AND %s ORDER BY stat_date ASC", $start, $end ), ARRAY_A); $map = []; for ($month = 1; $month <= 12; $month++) { $month_key = sprintf('%04d-%02d', $year, $month); $map[$month_key] = 0; } if (is_array($rows)) { foreach ($rows as $row) { $stat_date = isset($row['stat_date']) ? (string) $row['stat_date'] : ''; $month_key = substr($stat_date, 0, 7); if ($month_key !== '' && isset($map[$month_key])) { $map[$month_key] += (int) ($row['views'] ?? 0); } } } $data = []; foreach ($map as $month_key => $views) { $data[] = [ 'label' => wp_date('M', strtotime($month_key . '-01')), 'views' => (int) $views, ]; } return $data; } protected function is_unknown_only_dimension_rows(array $rows) { if (empty($rows)) { return true; } foreach ($rows as $row) { $value = strtolower(trim((string) ($row['dimension_value'] ?? ''))); $label = strtolower(trim((string) ($row['dimension_label'] ?? ''))); if (!in_array($value, ['', 'unknown'], true) && !in_array($label, ['', 'unknown'], true)) { return false; } } return true; } protected function format_percent(int $value, int $total) { if ($total <= 0) { return '0%'; } $percent = ($value / $total) * 100; return number_format_i18n($percent, 1) . '%'; } protected function render_referral_summary_table(array $summary) { $total = array_sum(array_map('intval', $summary)); ?>
format_percent((int) ($summary['direct'] ?? 0), $total)); ?>
format_percent((int) ($summary['search'] ?? 0), $total)); ?>
format_percent((int) ($summary['social'] ?? 0), $total)); ?>
format_percent((int) ($summary['website'] ?? 0), $total)); ?>
$row) : ?>
(' . esc_html($value) . ')'; } ?> format_percent($count, $total)); ?>
' . esc_html__('No tracked 404 views for this month.', 'ansico-stat-plugin') . '

'; return; } ?> $row) : ?>
format_external_link((string) ($row['page_url'] ?? '')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
' . esc_html__('No views tracked for this period.', 'ansico-stat-plugin') . '

'; return; } ?> $row) : ?>
format_post_link($post_id, $title); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
' . esc_html($label) . ''; if (empty($rows)) { echo '

' . esc_html__('No tracked referral URLs for this category in the selected month.', 'ansico-stat-plugin') . '

'; return; } ?>
format_external_link((string) $row['source_url']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> format_percent($count, $total)); ?>
date_days_ago(29); $start = isset($_GET['start_date']) ? $this->sanitize_stats_date((string) wp_unslash($_GET['start_date'])) : ''; $end = isset($_GET['end_date']) ? $this->sanitize_stats_date((string) wp_unslash($_GET['end_date'])) : ''; if ($start === '') { $start = $default_start; } if ($end === '') { $end = $default_end; } if (strtotime($start) > strtotime($end)) { [$start, $end] = [$end, $start]; } return [ 'start' => $start, 'end' => $end, 'days' => max(1, (int) floor((strtotime($end) - strtotime($start)) / DAY_IN_SECONDS) + 1), 'is_custom' => isset($_GET['start_date']) || isset($_GET['end_date']), ]; } protected function build_single_stats_admin_url(string $target_url, string $start_date = '', string $end_date = '') { $args = [ 'page' => self::MENU_SLUG_SINGLE, 'target_url' => $target_url !== '' ? $target_url : home_url('/'), ]; if ($start_date !== '') { $args['start_date'] = $start_date; } if ($end_date !== '') { $args['end_date'] = $end_date; } return add_query_arg($args, admin_url('admin.php')); } public function register_frontend_admin_bar_link($wp_admin_bar) { if (is_admin() || !is_admin_bar_showing() || !current_user_can('manage_options') || !$this->is_trackable_request()) { return; } $page_context = $this->get_current_page_context(); if (empty($page_context) || empty($page_context['url'])) { return; } $target_url = $this->normalize_target_url((string) $page_context['url']); if ($target_url === '') { return; } $wp_admin_bar->add_node([ 'id' => 'ansico-stat-single-page', 'parent' => 'top-secondary', 'title' => '' . esc_html__('Page stats', 'ansico-stat-plugin') . '', 'href' => esc_url($this->build_single_stats_admin_url($target_url)), 'meta' => [ 'class' => 'ansico-stat-admin-bar-link', 'title' => esc_attr__('Open statistics for this page', 'ansico-stat-plugin'), ], ]); } protected function resolve_single_stats_target(string $url) { global $wpdb; $normalized_url = $this->normalize_target_url($url); if ($normalized_url === '') { return [ 'success' => false, 'message' => __('Please enter a valid URL.', 'ansico-stat-plugin'), 'requested_url' => $url, 'normalized_url' => '', ]; } $site_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST)); $target_host = strtolower((string) wp_parse_url($normalized_url, PHP_URL_HOST)); if ($site_host === '' || $target_host === '' || preg_replace('/^www\./', '', $site_host) !== preg_replace('/^www\./', '', $target_host)) { return [ 'success' => false, 'message' => __('The URL must belong to this WordPress site.', 'ansico-stat-plugin'), 'requested_url' => $url, 'normalized_url' => $normalized_url, ]; } $post_id = url_to_postid($normalized_url); if ($post_id > 0) { $post = get_post($post_id); if ($this->is_trackable_post($post)) { return [ 'success' => true, 'requested_url' => $url, 'normalized_url' => $normalized_url, 'target' => [ 'kind' => 'singular', 'post_id' => (int) $post_id, 'post_type' => sanitize_key((string) $post->post_type), 'page_key' => 'post:' . (int) $post_id, 'page_type' => sanitize_key((string) $post->post_type), 'object_id' => (int) $post_id, 'label' => get_the_title($post_id) ?: __('(no title)', 'ansico-stat-plugin'), 'url' => get_permalink($post_id) ?: $normalized_url, 'resolved_via' => 'post', ], ]; } } $front_url = $this->normalize_target_url(home_url('/')); if ($normalized_url === $front_url) { return [ 'success' => true, 'requested_url' => $url, 'normalized_url' => $normalized_url, 'target' => [ 'kind' => 'archive', 'page_key' => 'front_page', 'page_type' => 'front_page', 'object_id' => 0, 'label' => __('Front page', 'ansico-stat-plugin'), 'url' => home_url('/'), 'resolved_via' => 'front_page', ], ]; } $posts_page_id = (int) get_option('page_for_posts'); if ($posts_page_id > 0) { $posts_page_url = $this->normalize_target_url((string) get_permalink($posts_page_id)); if ($posts_page_url !== '' && $normalized_url === $posts_page_url) { return [ 'success' => true, 'requested_url' => $url, 'normalized_url' => $normalized_url, 'target' => [ 'kind' => 'archive', 'page_key' => 'home', 'page_type' => 'home', 'object_id' => $posts_page_id, 'label' => __('Posts page', 'ansico-stat-plugin'), 'url' => get_permalink($posts_page_id) ?: $normalized_url, 'resolved_via' => 'posts_page', ], ]; } } $page_daily_table = self::page_daily_table_name(); $url_variants = array_values(array_unique(array_filter([ $normalized_url, untrailingslashit($normalized_url), trailingslashit($normalized_url), ]))); if (!empty($url_variants)) { $placeholders = implode(',', array_fill(0, count($url_variants), '%s')); $sql = "SELECT page_key, page_type, object_id, page_label, page_url\n FROM {$page_daily_table}\n WHERE page_url IN ({$placeholders})\n ORDER BY stat_date DESC\n LIMIT 1"; $row = $wpdb->get_row($wpdb->prepare($sql, $url_variants), ARRAY_A); if (is_array($row) && !empty($row['page_key'])) { return [ 'success' => true, 'requested_url' => $url, 'normalized_url' => $normalized_url, 'target' => [ 'kind' => 'archive', 'page_key' => (string) $row['page_key'], 'page_type' => sanitize_key((string) ($row['page_type'] ?? 'archive')), 'object_id' => !empty($row['object_id']) ? (int) $row['object_id'] : 0, 'label' => sanitize_text_field((string) ($row['page_label'] ?? $normalized_url)), 'url' => !empty($row['page_url']) ? esc_url_raw((string) $row['page_url']) : $normalized_url, 'resolved_via' => 'page_daily', ], ]; } } return [ 'success' => false, 'message' => __('No tracked page or post was found for that URL yet. Try opening the page on the site first so the plugin can register it, and then update this report again.', 'ansico-stat-plugin'), 'requested_url' => $url, 'normalized_url' => $normalized_url, ]; } protected function get_target_lifetime_views(array $target) { global $wpdb; if (($target['kind'] ?? '') === 'singular') { return (int) get_post_meta((int) $target['post_id'], self::TOTAL_META_KEY, true); } $table = self::page_daily_table_name(); $views = $wpdb->get_var($wpdb->prepare( "SELECT SUM(views) FROM {$table} WHERE page_key = %s", (string) $target['page_key'] )); return (int) $views; } protected function get_target_lifetime_unique_visitors(array $target) { global $wpdb; if (($target['kind'] ?? '') === 'singular') { $table = self::post_daily_table_name(); $uniques = $wpdb->get_var($wpdb->prepare( "SELECT SUM(unique_visitors) FROM {$table} WHERE post_id = %d", (int) $target['post_id'] )); return (int) $uniques; } $table = self::page_daily_table_name(); $uniques = $wpdb->get_var($wpdb->prepare( "SELECT SUM(unique_visitors) FROM {$table} WHERE page_key = %s", (string) $target['page_key'] )); return (int) $uniques; } protected function get_target_period_unique_visitors(array $target, string $start, string $end) { global $wpdb; if (($target['kind'] ?? '') === 'singular') { $table = self::post_daily_table_name(); $uniques = $wpdb->get_var($wpdb->prepare( "SELECT SUM(unique_visitors) FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s", (int) $target['post_id'], $start, $end )); return (int) $uniques; } $table = self::page_daily_table_name(); $uniques = $wpdb->get_var($wpdb->prepare( "SELECT SUM(unique_visitors) FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s", (string) $target['page_key'], $start, $end )); return (int) $uniques; } protected function get_target_daily_chart_data_between(array $target, string $start, string $end, string $metric = 'views') { global $wpdb; $metric = $metric === 'unique_visitors' ? 'unique_visitors' : 'views'; if (($target['kind'] ?? '') === 'singular') { $table = self::post_daily_table_name(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT stat_date, {$metric} AS metric_value FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC", (int) $target['post_id'], $start, $end ), ARRAY_A); } else { $table = self::page_daily_table_name(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT stat_date, {$metric} AS metric_value FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC", (string) $target['page_key'], $start, $end ), ARRAY_A); } $indexed = []; foreach ((array) $rows as $row) { $indexed[(string) $row['stat_date']] = (int) $row['metric_value']; } $data = []; $cursor = strtotime($start); $end_ts = strtotime($end); while ($cursor <= $end_ts) { $date = gmdate('Y-m-d', $cursor); $data[] = [ 'date' => $date, 'views' => $indexed[$date] ?? 0, ]; $cursor = strtotime('+1 day', $cursor); } return $data; } protected function get_target_monthly_chart_data_between(array $target, string $start, string $end) { global $wpdb; if (($target['kind'] ?? '') === 'singular') { $table = self::post_daily_table_name(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT DATE_FORMAT(stat_date, '%%Y-%%m') AS month_key, SUM(views) AS total_views FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s GROUP BY month_key ORDER BY month_key ASC", (int) $target['post_id'], $start, $end ), ARRAY_A); } else { $table = self::page_daily_table_name(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT DATE_FORMAT(stat_date, '%%Y-%%m') AS month_key, SUM(views) AS total_views FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s GROUP BY month_key ORDER BY month_key ASC", (string) $target['page_key'], $start, $end ), ARRAY_A); } $indexed = []; foreach ((array) $rows as $row) { $indexed[(string) $row['month_key']] = (int) $row['total_views']; } $data = []; $cursor = strtotime(gmdate('Y-m-01', strtotime($start))); $end_month = strtotime(gmdate('Y-m-01', strtotime($end))); while ($cursor <= $end_month) { $month_key = gmdate('Y-m', $cursor); $data[] = [ 'label' => wp_date('M Y', $cursor), 'views' => $indexed[$month_key] ?? 0, ]; $cursor = strtotime('+1 month', $cursor); } return $data; } protected function get_target_weekday_breakdown_between(array $target, string $start, string $end) { $daily_data = $this->get_target_daily_chart_data_between($target, $start, $end, 'views'); $weekday_rows = [ __('Mon', 'ansico-stat-plugin') => 0, __('Tue', 'ansico-stat-plugin') => 0, __('Wed', 'ansico-stat-plugin') => 0, __('Thu', 'ansico-stat-plugin') => 0, __('Fri', 'ansico-stat-plugin') => 0, __('Sat', 'ansico-stat-plugin') => 0, __('Sun', 'ansico-stat-plugin') => 0, ]; $weekday_keys = array_keys($weekday_rows); foreach ($daily_data as $row) { $timestamp = strtotime((string) ($row['date'] ?? '')); if (!$timestamp) { continue; } $weekday_index = (int) gmdate('N', $timestamp) - 1; if (isset($weekday_keys[$weekday_index])) { $weekday_rows[$weekday_keys[$weekday_index]] += (int) ($row['views'] ?? 0); } } $data = []; foreach ($weekday_rows as $label => $views) { $data[] = [ 'label' => $label, 'views' => (int) $views, ]; } return $data; } protected function render_dual_chart_markup(array $views_data, array $unique_data, string $chart_id, string $aria_label = '') { if ($aria_label === '') { $aria_label = __('Views and unique visitors chart', 'ansico-stat-plugin'); } $count = max(count($views_data), count($unique_data)); if ($count < 1) { return '

' . esc_html__('No chart data available.', 'ansico-stat-plugin') . '

'; } $values = []; foreach ($views_data as $row) { $values[] = (int) ($row['views'] ?? 0); } foreach ($unique_data as $row) { $values[] = (int) ($row['views'] ?? 0); } $max = max(1, !empty($values) ? max($values) : 1); $width = 920; $height = 240; $chart_top = 20; $chart_bottom = 185; $chart_left = 26; $chart_right = 830; $stepX = $count > 1 ? ($chart_right - $chart_left) / ($count - 1) : ($chart_right - $chart_left); $build_points = static function(array $series, string $series_label) use ($chart_top, $chart_bottom, $chart_left, $stepX, $max) { $points = []; foreach (array_values($series) as $index => $row) { $value = (int) ($row['views'] ?? 0); $x = $chart_left + ($stepX * $index); $y = $value > 0 ? $chart_top + (($chart_bottom - $chart_top) * (1 - ($value / $max))) : $chart_bottom; $label = (string) ($row['label'] ?? $row['date'] ?? ''); $points[] = [ 'x' => $x, 'y' => $y, 'label' => $label, 'value' => $value, 'series_label' => $series_label, ]; } return $points; }; $view_points = $build_points($views_data, __('Views', 'ansico-stat-plugin')); $unique_points = $build_points($unique_data, __('Unique visitors', 'ansico-stat-plugin')); $view_path = implode(' ', array_map(static function($point) { return $point['x'] . ',' . $point['y']; }, $view_points)); $unique_path = implode(' ', array_map(static function($point) { return $point['x'] . ',' . $point['y']; }, $unique_points)); ob_start(); ?>
date_days_ago($days - 1); $end = current_time('Y-m-d'); if (($target['kind'] ?? '') === 'singular') { $table = self::post_daily_table_name(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT stat_date, unique_visitors FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC", (int) $target['post_id'], $start, $end ), ARRAY_A); } else { $table = self::page_daily_table_name(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT stat_date, unique_visitors FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC", (string) $target['page_key'], $start, $end ), ARRAY_A); } $indexed = []; foreach ((array) $rows as $row) { $indexed[(string) $row['stat_date']] = (int) $row['unique_visitors']; } $data = []; $cursor = strtotime($start); $end_ts = strtotime($end); while ($cursor <= $end_ts) { $date = gmdate('Y-m-d', $cursor); $data[] = [ 'date' => $date, 'views' => $indexed[$date] ?? 0, ]; $cursor = strtotime('+1 day', $cursor); } return $data; } protected function get_target_period_views(array $target, string $start, string $end) { global $wpdb; if (($target['kind'] ?? '') === 'singular') { $table = self::post_daily_table_name(); $views = $wpdb->get_var($wpdb->prepare( "SELECT SUM(views) FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s", (int) $target['post_id'], $start, $end )); return (int) $views; } $table = self::page_daily_table_name(); $views = $wpdb->get_var($wpdb->prepare( "SELECT SUM(views) FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s", (string) $target['page_key'], $start, $end )); return (int) $views; } protected function get_target_daily_chart_data(array $target, int $days = 60) { global $wpdb; $days = max(1, $days); $start = $this->date_days_ago($days - 1); $end = current_time('Y-m-d'); if (($target['kind'] ?? '') === 'singular') { $table = self::post_daily_table_name(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT stat_date, views FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC", (int) $target['post_id'], $start, $end ), ARRAY_A); } else { $table = self::page_daily_table_name(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT stat_date, views FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC", (string) $target['page_key'], $start, $end ), ARRAY_A); } $indexed = []; foreach ((array) $rows as $row) { $indexed[(string) $row['stat_date']] = (int) $row['views']; } $data = []; $cursor = strtotime($start); $end_ts = strtotime($end); while ($cursor <= $end_ts) { $date = gmdate('Y-m-d', $cursor); $data[] = [ 'date' => $date, 'views' => $indexed[$date] ?? 0, ]; $cursor = strtotime('+1 day', $cursor); } return $data; } protected function get_target_monthly_chart_data(array $target, int $months = 12) { global $wpdb; $months = max(1, $months); $current_month = strtotime(wp_date('Y-m-01', current_time('timestamp'))); $start_month = strtotime('-' . ($months - 1) . ' months', $current_month); $start = wp_date('Y-m-01', $start_month); $end = wp_date('Y-m-t', current_time('timestamp')); if (($target['kind'] ?? '') === 'singular') { $table = self::post_daily_table_name(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT DATE_FORMAT(stat_date, '%%Y-%%m') AS month_key, SUM(views) AS total_views\n FROM {$table}\n WHERE post_id = %d AND stat_date BETWEEN %s AND %s\n GROUP BY month_key\n ORDER BY month_key ASC", (int) $target['post_id'], $start, $end ), ARRAY_A); } else { $table = self::page_daily_table_name(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT DATE_FORMAT(stat_date, '%%Y-%%m') AS month_key, SUM(views) AS total_views\n FROM {$table}\n WHERE page_key = %s AND stat_date BETWEEN %s AND %s\n GROUP BY month_key\n ORDER BY month_key ASC", (string) $target['page_key'], $start, $end ), ARRAY_A); } $indexed = []; foreach ((array) $rows as $row) { $indexed[(string) $row['month_key']] = (int) $row['total_views']; } $data = []; $cursor = $start_month; while ($cursor <= $current_month) { $month_key = wp_date('Y-m', $cursor); $data[] = [ 'label' => wp_date('M Y', $cursor), 'views' => $indexed[$month_key] ?? 0, ]; $cursor = strtotime('+1 month', $cursor); } return $data; } protected function get_target_best_day(array $target) { global $wpdb; if (($target['kind'] ?? '') === 'singular') { $table = self::post_daily_table_name(); $row = $wpdb->get_row($wpdb->prepare( "SELECT stat_date, views FROM {$table} WHERE post_id = %d ORDER BY views DESC, stat_date ASC LIMIT 1", (int) $target['post_id'] ), ARRAY_A); } else { $table = self::page_daily_table_name(); $row = $wpdb->get_row($wpdb->prepare( "SELECT stat_date, views FROM {$table} WHERE page_key = %s ORDER BY views DESC, stat_date ASC LIMIT 1", (string) $target['page_key'] ), ARRAY_A); } return is_array($row) ? $row : []; } protected function get_target_first_tracked_date(array $target) { global $wpdb; if (($target['kind'] ?? '') === 'singular') { $table = self::post_daily_table_name(); $date = $wpdb->get_var($wpdb->prepare( "SELECT MIN(stat_date) FROM {$table} WHERE post_id = %d", (int) $target['post_id'] )); } else { $table = self::page_daily_table_name(); $date = $wpdb->get_var($wpdb->prepare( "SELECT MIN(stat_date) FROM {$table} WHERE page_key = %s", (string) $target['page_key'] )); } return is_string($date) ? $date : ''; } protected function get_target_total_tracked_days(array $target) { global $wpdb; if (($target['kind'] ?? '') === 'singular') { $table = self::post_daily_table_name(); $count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE post_id = %d", (int) $target['post_id'] )); } else { $table = self::page_daily_table_name(); $count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE page_key = %s", (string) $target['page_key'] )); } return (int) $count; } protected function get_target_average_unique_visitors_per_day(array $target, int $days) { $days = max(1, $days); $uniques = $this->get_target_period_unique_visitors($target, $this->date_days_ago($days - 1), current_time('Y-m-d')); return $uniques / $days; } protected function get_target_unique_period_comparison(array $target, int $days) { $days = max(1, $days); $current_end = current_time('Y-m-d'); $current_start = $this->date_days_ago($days - 1); $previous_end = $this->date_days_ago($days); $previous_start = $this->date_days_ago(($days * 2) - 1); $current_uniques = $this->get_target_period_unique_visitors($target, $current_start, $current_end); $previous_uniques = $this->get_target_period_unique_visitors($target, $previous_start, $previous_end); $change = $current_uniques - $previous_uniques; $percent_change = null; if ($previous_uniques > 0) { $percent_change = (($current_uniques - $previous_uniques) / $previous_uniques) * 100; } elseif ($current_uniques > 0) { $percent_change = 100.0; } return [ 'days' => $days, 'current_views' => (int) $current_uniques, 'previous_views' => (int) $previous_uniques, 'change' => (int) $change, 'percent_change' => $percent_change, 'current_start' => $current_start, 'current_end' => $current_end, 'previous_start' => $previous_start, 'previous_end' => $previous_end, ]; } protected function get_target_average_views_per_day(array $target, int $days) { $days = max(1, $days); $views = $this->get_target_period_views($target, $this->date_days_ago($days - 1), current_time('Y-m-d')); return $views / $days; } protected function get_target_period_comparison(array $target, int $days) { $days = max(1, $days); $current_end = current_time('Y-m-d'); $current_start = $this->date_days_ago($days - 1); $previous_end = $this->date_days_ago($days); $previous_start = $this->date_days_ago(($days * 2) - 1); $current_views = $this->get_target_period_views($target, $current_start, $current_end); $previous_views = $this->get_target_period_views($target, $previous_start, $previous_end); $change = $current_views - $previous_views; $percent_change = null; if ($previous_views > 0) { $percent_change = (($current_views - $previous_views) / $previous_views) * 100; } elseif ($current_views > 0) { $percent_change = 100.0; } return [ 'days' => $days, 'current_views' => (int) $current_views, 'previous_views' => (int) $previous_views, 'change' => (int) $change, 'percent_change' => $percent_change, 'current_start' => $current_start, 'current_end' => $current_end, 'previous_start' => $previous_start, 'previous_end' => $previous_end, ]; } protected function format_change_percent($percent_change) { if ($percent_change === null) { return __('New data', 'ansico-stat-plugin'); } $prefix = $percent_change > 0 ? '+' : ''; return $prefix . number_format_i18n((float) $percent_change, 1) . '%'; } protected function get_target_weekday_breakdown(array $target, int $days = 90) { $days = max(1, $days); $daily_data = $this->get_target_daily_chart_data($target, $days); $weekday_rows = [ __('Mon', 'ansico-stat-plugin') => 0, __('Tue', 'ansico-stat-plugin') => 0, __('Wed', 'ansico-stat-plugin') => 0, __('Thu', 'ansico-stat-plugin') => 0, __('Fri', 'ansico-stat-plugin') => 0, __('Sat', 'ansico-stat-plugin') => 0, __('Sun', 'ansico-stat-plugin') => 0, ]; $weekday_keys = array_keys($weekday_rows); foreach ($daily_data as $row) { $timestamp = strtotime((string) ($row['date'] ?? '')); if (!$timestamp) { continue; } $weekday_index = (int) gmdate('N', $timestamp) - 1; if (!isset($weekday_keys[$weekday_index])) { continue; } $weekday_rows[$weekday_keys[$weekday_index]] += (int) ($row['views'] ?? 0); } $data = []; foreach ($weekday_rows as $label => $views) { $data[] = [ 'label' => $label, 'views' => (int) $views, ]; } return $data; } protected function build_single_export_url(string $target_url, string $start_date = '', string $end_date = '') { $args = [ 'action' => 'ansico_stat_export_single_csv', 'target_url' => $target_url, ]; if ($start_date !== '') { $args['start_date'] = $start_date; } if ($end_date !== '') { $args['end_date'] = $end_date; } return wp_nonce_url( add_query_arg($args, admin_url('admin-post.php')), 'ansico_stat_export_single_csv' ); } protected function render_single_stats_summary_cards(array $items) { echo '
'; foreach ($items as $item) { echo '
'; echo '
' . esc_html((string) ($item['label'] ?? '')) . '
'; echo '
' . esc_html((string) ($item['value'] ?? '')) . '
'; if (!empty($item['description'])) { echo '
' . esc_html((string) $item['description']) . '
'; } echo '
'; } echo '
'; } public function render_single_page_stats_page() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to access this page.', 'ansico-stat-plugin')); } $requested_url = $this->get_requested_single_stats_url(); $date_range = $this->get_single_stats_date_range(); $range_start = (string) $date_range['start']; $range_end = (string) $date_range['end']; $range_days = (int) $date_range['days']; $result = $this->resolve_single_stats_target($requested_url); $target = !empty($result['success']) ? (array) $result['target'] : []; $daily_chart = !empty($target) ? $this->get_target_daily_chart_data_between($target, $range_start, $range_end, 'views') : []; $unique_daily_chart = !empty($target) ? $this->get_target_daily_chart_data_between($target, $range_start, $range_end, 'unique_visitors') : []; $monthly_chart = !empty($target) ? $this->get_target_monthly_chart_data_between($target, $range_start, $range_end) : []; $weekday_chart = !empty($target) ? $this->get_target_weekday_breakdown_between($target, $range_start, $range_end) : []; $lifetime_views = !empty($target) ? $this->get_target_lifetime_views($target) : 0; $lifetime_unique_visitors = !empty($target) ? $this->get_target_lifetime_unique_visitors($target) : 0; $views_today = !empty($target) ? $this->get_target_period_views($target, current_time('Y-m-d'), current_time('Y-m-d')) : 0; $unique_today = !empty($target) ? $this->get_target_period_unique_visitors($target, current_time('Y-m-d'), current_time('Y-m-d')) : 0; $range_views = !empty($target) ? $this->get_target_period_views($target, $range_start, $range_end) : 0; $range_unique = !empty($target) ? $this->get_target_period_unique_visitors($target, $range_start, $range_end) : 0; $best_day = !empty($target) ? $this->get_target_best_day($target) : []; $first_tracked = !empty($target) ? $this->get_target_first_tracked_date($target) : ''; $tracked_days = !empty($target) ? $this->get_target_total_tracked_days($target) : 0; $avg_range = $range_days > 0 ? ($range_views / $range_days) : 0; $avg_unique_range = $range_days > 0 ? ($range_unique / $range_days) : 0; $compare_range = !empty($target) ? $this->get_target_period_comparison($target, $range_days) : []; $unique_compare_range = !empty($target) ? $this->get_target_unique_period_comparison($target, $range_days) : []; $views_7 = !empty($target) ? $this->get_target_period_views($target, $this->date_days_ago(6), current_time('Y-m-d')) : 0; $views_30 = !empty($target) ? $this->get_target_period_views($target, $this->date_days_ago(29), current_time('Y-m-d')) : 0; $unique_7 = !empty($target) ? $this->get_target_period_unique_visitors($target, $this->date_days_ago(6), current_time('Y-m-d')) : 0; $unique_30 = !empty($target) ? $this->get_target_period_unique_visitors($target, $this->date_days_ago(29), current_time('Y-m-d')) : 0; $export_url = !empty($target) ? $this->build_single_export_url($requested_url, $range_start, $range_end) : ''; $reset_range_url = $this->build_single_stats_admin_url($requested_url); $post_type_label = ''; if (!empty($target) && ($target['kind'] ?? '') === 'singular') { $post_type_obj = get_post_type_object((string) ($target['post_type'] ?? '')); $post_type_label = $post_type_obj && !empty($post_type_obj->labels->singular_name) ? (string) $post_type_obj->labels->singular_name : (string) ($target['post_type'] ?? ''); } ?>

render_single_stats_summary_cards([ [ 'label' => __('Views in selected period', 'ansico-stat-plugin'), 'value' => number_format_i18n($range_views), 'description' => sprintf(__('Avg. %s per day', 'ansico-stat-plugin'), number_format_i18n($avg_range, 1)), ], [ 'label' => __('Unique visitors in selected period', 'ansico-stat-plugin'), 'value' => number_format_i18n($range_unique), 'description' => sprintf(__('Avg. %s per day', 'ansico-stat-plugin'), number_format_i18n($avg_unique_range, 1)), ], [ 'label' => __('Views today', 'ansico-stat-plugin'), 'value' => number_format_i18n($views_today), 'description' => __('Today only', 'ansico-stat-plugin'), ], [ 'label' => __('Unique visitors today', 'ansico-stat-plugin'), 'value' => number_format_i18n($unique_today), 'description' => __('Today only', 'ansico-stat-plugin'), ], [ 'label' => __('Views last 7 days', 'ansico-stat-plugin'), 'value' => number_format_i18n($views_7), 'description' => __('Rolling 7-day total', 'ansico-stat-plugin'), ], [ 'label' => __('Unique visitors last 7 days', 'ansico-stat-plugin'), 'value' => number_format_i18n($unique_7), 'description' => __('Rolling 7-day total', 'ansico-stat-plugin'), ], [ 'label' => __('Views last 30 days', 'ansico-stat-plugin'), 'value' => number_format_i18n($views_30), 'description' => __('Rolling 30-day total', 'ansico-stat-plugin'), ], [ 'label' => __('Unique visitors last 30 days', 'ansico-stat-plugin'), 'value' => number_format_i18n($unique_30), 'description' => __('Rolling 30-day total', 'ansico-stat-plugin'), ], [ 'label' => __('Lifetime views', 'ansico-stat-plugin'), 'value' => number_format_i18n($lifetime_views), 'description' => $first_tracked !== '' ? sprintf(__('Tracked since %s', 'ansico-stat-plugin'), wp_date(get_option('date_format'), strtotime($first_tracked))) : '', ], [ 'label' => __('Lifetime unique visitors', 'ansico-stat-plugin'), 'value' => number_format_i18n($lifetime_unique_visitors), 'description' => __('Stored from version 1.0.0.6 and forward', 'ansico-stat-plugin'), ], [ 'label' => __('Best day', 'ansico-stat-plugin'), 'value' => !empty($best_day['views']) ? number_format_i18n((int) $best_day['views']) : '0', 'description' => !empty($best_day['stat_date']) ? wp_date(get_option('date_format'), strtotime((string) $best_day['stat_date'])) : __('No tracked data yet', 'ansico-stat-plugin'), ], [ 'label' => __('Tracked days with views', 'ansico-stat-plugin'), 'value' => number_format_i18n($tracked_days), 'description' => __('Days stored in the daily statistics table', 'ansico-stat-plugin'), ], ]); ?>

render_dual_chart_markup($daily_chart, $unique_daily_chart, 'ansico-single-target-dual-chart', __('Views and unique visitors for selected page', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

render_chart_markup($daily_chart, 'ansico-single-target-daily-chart'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

render_chart_markup($unique_daily_chart, 'ansico-single-target-unique-daily-chart'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

render_bar_chart_markup($monthly_chart, 'ansico-single-target-monthly-chart', __('Monthly totals for selected page', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

render_bar_chart_markup($weekday_chart, 'ansico-single-target-weekday-chart', __('Views by weekday for selected page', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

format_change_percent($compare_range['percent_change'] ?? null)); ?>
format_change_percent($unique_compare_range['percent_change'] ?? null)); ?>
get_settings(); ?>

render_monthly_stats_page(); } public function render_monthly_stats_page() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to access this page.', 'ansico-stat-plugin')); } $settings = $this->get_settings(); $top_rows = max(1, min(100, (int) $settings['top_list_rows'])); $referral_rows_limit = max(1, min(200, (int) $settings['referral_rows'])); $chart_data = $this->get_daily_chart_data(60); $selected_month = $this->get_selected_month(); $monthly_rows = $this->get_monthly_top_posts($selected_month, $top_rows); $referral_summary = $this->get_referral_summary_for_month($selected_month); $pie_summary = [ __('Direct', 'ansico-stat-plugin') => (int) $referral_summary['direct'], __('Search engines', 'ansico-stat-plugin') => (int) $referral_summary['search'], __('Social media', 'ansico-stat-plugin') => (int) $referral_summary['social'], __('Other websites', 'ansico-stat-plugin') => (int) $referral_summary['website'], ]; $search_referrals = $this->get_referral_sources_for_month($selected_month, 'search', $referral_rows_limit); $social_referrals = $this->get_referral_sources_for_month($selected_month, 'social', $referral_rows_limit); $website_referrals = $this->get_referral_sources_for_month($selected_month, 'website', $referral_rows_limit); $monthly_404_rows = $this->get_top_404_pages_for_month($selected_month, $top_rows); $monthly_country_rows = $this->get_dimension_rows_for_month($selected_month, 'country', $top_rows); $monthly_device_rows = $this->get_dimension_rows_for_month($selected_month, 'device', max(3, $top_rows)); $monthly_browser_rows = $this->get_dimension_rows_for_month($selected_month, 'browser', $top_rows); $monthly_os_rows = $this->get_dimension_rows_for_month($selected_month, 'os', $top_rows); ?>

render_chart_markup($chart_data, 'ansico-stat-admin-chart'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

render_month_navigation($selected_month); ?> render_top_posts_table($monthly_rows, __('Monthly views', 'ansico-stat-plugin')); ?>

render_month_navigation($selected_month); ?> render_top_404_table($monthly_404_rows); ?>

render_month_navigation($selected_month); ?>
render_referral_summary_table($referral_summary); ?>
render_pie_chart_markup($pie_summary, 'ansico-monthly-referral-pie', __('Monthly referred visitors', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
render_referral_sources_table($search_referrals, __('Search engine referral URLs', 'ansico-stat-plugin')); ?>
render_referral_sources_table($social_referrals, __('Social media referral URLs', 'ansico-stat-plugin')); ?>
render_referral_sources_table($website_referrals, __('Other website referral URLs', 'ansico-stat-plugin')); ?>
is_unknown_only_dimension_rows($monthly_country_rows)) : ?>

render_month_navigation($selected_month); ?>

render_dimension_table($monthly_country_rows, __('Country', 'ansico-stat-plugin')); ?>
render_bar_chart_markup($monthly_country_rows, 'ansico-monthly-country-chart', __('Monthly countries', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

render_month_navigation($selected_month); ?>
render_dimension_table($monthly_device_rows, __('Device type', 'ansico-stat-plugin')); ?>
render_pie_chart_markup(array_column($monthly_device_rows, 'total_views', 'dimension_value'), 'ansico-monthly-device-pie', __('Monthly device types', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

render_month_navigation($selected_month); ?>
render_dimension_table($monthly_browser_rows, __('Browser', 'ansico-stat-plugin')); ?>
render_bar_chart_markup($monthly_browser_rows, 'ansico-monthly-browser-chart', __('Monthly browsers', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

render_month_navigation($selected_month); ?>
render_dimension_table($monthly_os_rows, __('Operating system', 'ansico-stat-plugin')); ?>
render_bar_chart_markup($monthly_os_rows, 'ansico-monthly-os-chart', __('Monthly operating systems', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
get_settings(); $top_rows = max(1, min(100, (int) $settings['top_list_rows'])); $selected_year = $this->get_selected_year(); $yearly_rows = $this->get_yearly_top_posts($selected_year, $top_rows); $yearly_country_rows = $this->get_dimension_rows_for_year($selected_year, 'country', $top_rows); $yearly_device_rows = $this->get_dimension_rows_for_year($selected_year, 'device', max(3, $top_rows)); $yearly_browser_rows = $this->get_dimension_rows_for_year($selected_year, 'browser', $top_rows); $yearly_os_rows = $this->get_dimension_rows_for_year($selected_year, 'os', $top_rows); $yearly_referral_summary = $this->get_referral_summary_for_year($selected_year); $yearly_month_chart = $this->get_monthly_totals_for_year($selected_year); ?>

render_year_navigation($selected_year); ?> render_bar_chart_markup($yearly_month_chart, 'ansico-yearly-month-bar', __('Yearly views by month', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

render_year_navigation($selected_year); ?> render_top_posts_table($yearly_rows, __('Yearly views', 'ansico-stat-plugin')); ?>

render_year_navigation($selected_year); ?>
render_referral_summary_table([ 'direct' => (int) ($yearly_referral_summary['Direct'] ?? 0), 'search' => (int) ($yearly_referral_summary['Search engines'] ?? 0), 'social' => (int) ($yearly_referral_summary['Social media'] ?? 0), 'website' => (int) ($yearly_referral_summary['Other websites'] ?? 0), ]); ?>
render_pie_chart_markup($yearly_referral_summary, 'ansico-yearly-referral-pie', __('Yearly referred visitors', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
is_unknown_only_dimension_rows($yearly_country_rows)) : ?>

render_year_navigation($selected_year); ?>
render_dimension_table($yearly_country_rows, __('Country', 'ansico-stat-plugin')); ?>
render_bar_chart_markup($yearly_country_rows, 'ansico-yearly-country-chart', __('Yearly countries', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

render_year_navigation($selected_year); ?>
render_dimension_table($yearly_device_rows, __('Device type', 'ansico-stat-plugin')); ?>
render_pie_chart_markup(array_column($yearly_device_rows, 'total_views', 'dimension_value'), 'ansico-yearly-device-pie', __('Yearly device types', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

render_year_navigation($selected_year); ?>
render_dimension_table($yearly_browser_rows, __('Browser', 'ansico-stat-plugin')); ?>
render_bar_chart_markup($yearly_browser_rows, 'ansico-yearly-browser-chart', __('Yearly browsers', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

render_year_navigation($selected_year); ?>
render_dimension_table($yearly_os_rows, __('Operating system', 'ansico-stat-plugin')); ?>
render_bar_chart_markup($yearly_os_rows, 'ansico-yearly-os-chart', __('Yearly operating systems', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
resolve_single_stats_target($requested_url); if (empty($result['success']) || empty($result['target'])) { wp_die(esc_html__('No tracked page could be resolved for export.', 'ansico-stat-plugin')); } $target = (array) $result['target']; $start_date = isset($_GET['start_date']) ? $this->sanitize_stats_date((string) wp_unslash($_GET['start_date'])) : ''; $end_date = isset($_GET['end_date']) ? $this->sanitize_stats_date((string) wp_unslash($_GET['end_date'])) : ''; if ($start_date === '') { $start_date = $this->date_days_ago(29); } if ($end_date === '') { $end_date = current_time('Y-m-d'); } if (strtotime($start_date) > strtotime($end_date)) { [$start_date, $end_date] = [$end_date, $start_date]; } $daily_rows = $this->get_target_daily_chart_data_between($target, $start_date, $end_date, 'views'); $range_days = max(1, (int) floor((strtotime($end_date) - strtotime($start_date)) / DAY_IN_SECONDS) + 1); $compare_7 = $this->get_target_period_comparison($target, 7); $compare_30 = $this->get_target_period_comparison($target, 30); $compare_range = $this->get_target_period_comparison($target, $range_days); $unique_compare_7 = $this->get_target_unique_period_comparison($target, 7); $unique_compare_30 = $this->get_target_unique_period_comparison($target, 30); $unique_compare_range = $this->get_target_unique_period_comparison($target, $range_days); $filename_slug = sanitize_title((string) ($target['label'] ?? 'page-stats')); if ($filename_slug === '') { $filename_slug = 'page-stats'; } $filename = 'ansico-single-page-' . $filename_slug . '-' . wp_date('Y-m-d-H-i-s') . '.csv'; nocache_headers(); header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename=' . $filename); $output = fopen('php://output', 'w'); fputcsv($output, ['Selected URL', (string) ($target['url'] ?? '')]); fputcsv($output, ['Label', (string) ($target['label'] ?? '')]); fputcsv($output, ['Type', (string) (($target['kind'] ?? '') === 'singular' ? ($target['post_type'] ?? 'post') : ($target['page_type'] ?? 'page'))]); fputcsv($output, ['Selected period start', $start_date]); fputcsv($output, ['Selected period end', $end_date]); fputcsv($output, ['Selected period views', (int) $this->get_target_period_views($target, $start_date, $end_date)]); fputcsv($output, ['Selected period unique visitors', (int) $this->get_target_period_unique_visitors($target, $start_date, $end_date)]); fputcsv($output, ['Previous comparable period views', (int) ($compare_range['previous_views'] ?? 0)]); fputcsv($output, ['Previous comparable period unique visitors', (int) ($unique_compare_range['previous_views'] ?? 0)]); fputcsv($output, ['Lifetime views', (int) $this->get_target_lifetime_views($target)]); fputcsv($output, ['Views today', (int) $this->get_target_period_views($target, current_time('Y-m-d'), current_time('Y-m-d'))]); fputcsv($output, ['Unique visitors today', (int) $this->get_target_period_unique_visitors($target, current_time('Y-m-d'), current_time('Y-m-d'))]); fputcsv($output, ['Views last 7 days', (int) ($compare_7['current_views'] ?? 0)]); fputcsv($output, ['Unique visitors last 7 days', (int) ($unique_compare_7['current_views'] ?? 0)]); fputcsv($output, ['Previous 7 days', (int) ($compare_7['previous_views'] ?? 0)]); fputcsv($output, ['Previous unique visitors 7 days', (int) ($unique_compare_7['previous_views'] ?? 0)]); fputcsv($output, ['Views last 30 days', (int) ($compare_30['current_views'] ?? 0)]); fputcsv($output, ['Unique visitors last 30 days', (int) ($unique_compare_30['current_views'] ?? 0)]); fputcsv($output, ['Previous 30 days', (int) ($compare_30['previous_views'] ?? 0)]); fputcsv($output, ['Previous unique visitors 30 days', (int) ($unique_compare_30['previous_views'] ?? 0)]); fputcsv($output, []); fputcsv($output, ['Date', 'Views', 'Unique visitors']); foreach ($daily_rows as $row) { fputcsv($output, [ (string) ($row['date'] ?? ''), (int) ($row['views'] ?? 0), (int) $this->get_target_period_unique_visitors($target, (string) ($row['date'] ?? ''), (string) ($row['date'] ?? '')), ]); } fclose($output); exit; } public function handle_export_csv() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to export statistics.', 'ansico-stat-plugin')); } check_admin_referer('ansico_stat_export_csv'); $filename = 'ansico-stats-' . wp_date('Y-m-d-H-i-s') . '.csv'; $install_month = $this->get_install_month_key(); $current_month = wp_date('Y-m', current_time('timestamp')); nocache_headers(); header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename=' . $filename); $output = fopen('php://output', 'w'); fputcsv($output, ['Period type', 'Period', 'Rank', 'Title', 'Post ID', 'Post Type', 'Views', 'Lifetime Views', 'Permalink']); $cursor = strtotime($install_month . '-01'); $end = strtotime($current_month . '-01'); while ($cursor <= $end) { $month_key = wp_date('Y-m', $cursor); $rows = $this->get_monthly_top_posts($month_key, 10); foreach ($rows as $index => $row) { $post_id = (int) $row['post_id']; fputcsv($output, [ 'month', $month_key, $index + 1, get_the_title($post_id) ?: '(no title)', $post_id, get_post_type($post_id), (int) $row['period_views'], (int) get_post_meta($post_id, self::TOTAL_META_KEY, true), get_permalink($post_id), ]); } $cursor = strtotime('+1 month', $cursor); } for ($year = $this->get_install_year(); $year <= (int) wp_date('Y', current_time('timestamp')); $year++) { $rows = $this->get_yearly_top_posts($year, 10); foreach ($rows as $index => $row) { $post_id = (int) $row['post_id']; fputcsv($output, [ 'year', $year, $index + 1, get_the_title($post_id) ?: '(no title)', $post_id, get_post_type($post_id), (int) $row['period_views'], (int) get_post_meta($post_id, self::TOTAL_META_KEY, true), get_permalink($post_id), ]); } } fclose($output); exit; } public function handle_save_settings() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to save these settings.', 'ansico-stat-plugin')); } check_admin_referer('ansico_stat_save_settings'); $allowed_post_types = $this->get_default_track_post_types(); $submitted_post_types = isset($_POST['track_post_types']) && is_array($_POST['track_post_types']) ? array_map('sanitize_key', wp_unslash($_POST['track_post_types'])) : []; $submitted_post_types = array_values(array_intersect($allowed_post_types, $submitted_post_types)); $visitor_counting_mode = isset($_POST['visitor_counting_mode']) ? sanitize_key(wp_unslash($_POST['visitor_counting_mode'])) : 'count_all'; if (!in_array($visitor_counting_mode, ['count_all', 'exclude_admins', 'exclude_all_logged_in'], true)) { $visitor_counting_mode = 'count_all'; } $settings = [ 'visitor_counting_mode' => $visitor_counting_mode, 'exclude_known_bots' => isset($_POST['exclude_known_bots']) ? 1 : 0, 'top_list_rows' => isset($_POST['top_list_rows']) ? max(1, min(100, (int) $_POST['top_list_rows'])) : 10, 'referral_rows' => isset($_POST['referral_rows']) ? max(1, min(200, (int) $_POST['referral_rows'])) : 25, 'revisit_minutes' => isset($_POST['revisit_minutes']) ? max(1, min(10080, (int) $_POST['revisit_minutes'])) : 30, 'frontend_label' => isset($_POST['frontend_label']) ? sanitize_text_field(wp_unslash($_POST['frontend_label'])) : 'Views', 'track_post_types' => $submitted_post_types, 'track_front_page' => isset($_POST['track_front_page']) ? 1 : 0, 'track_posts_page' => isset($_POST['track_posts_page']) ? 1 : 0, 'track_archives' => isset($_POST['track_archives']) ? 1 : 0, 'track_search' => isset($_POST['track_search']) ? 1 : 0, 'track_404' => isset($_POST['track_404']) ? 1 : 0, ]; update_option(self::OPTION_KEY, $settings, false); wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&settings-updated=true')); exit; } public function handle_reset_all_views() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to reset statistics.', 'ansico-stat-plugin')); } check_admin_referer('ansico_stat_reset_all'); global $wpdb; $wpdb->query($wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s", self::TOTAL_META_KEY )); $wpdb->query('TRUNCATE TABLE ' . self::daily_table_name()); $wpdb->query('TRUNCATE TABLE ' . self::post_daily_table_name()); $wpdb->query('TRUNCATE TABLE ' . self::page_daily_table_name()); $wpdb->query('TRUNCATE TABLE ' . self::referral_table_name()); $wpdb->query('TRUNCATE TABLE ' . self::dimension_table_name()); update_option(self::INSTALL_OPTION_KEY, current_time('Y-m-d'), false); wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&reset=success')); exit; } public function register_admin_columns() { if (!is_admin()) { return; } $post_types = get_post_types(['show_ui' => true], 'names'); foreach ($post_types as $post_type) { add_filter("manage_{$post_type}_posts_columns", [$this, 'add_view_columns']); add_action("manage_{$post_type}_posts_custom_column", [$this, 'render_view_columns'], 10, 2); add_filter("manage_edit-{$post_type}_sortable_columns", [$this, 'register_sortable_view_columns']); } add_filter('manage_pages_columns', [$this, 'add_view_columns']); add_action('manage_pages_custom_column', [$this, 'render_view_columns'], 10, 2); add_filter('manage_edit-page_sortable_columns', [$this, 'register_sortable_view_columns']); } public function add_view_columns($columns) { $new_columns = []; foreach ($columns as $key => $label) { $new_columns[$key] = $label; if ($key === 'title') { $new_columns['ansico_lifetime_views'] = __('Post Views (Lifetime)', 'ansico-stat-plugin'); $new_columns['ansico_latest_month_views'] = __('Post Views (Latest Month)', 'ansico-stat-plugin'); } } if (!isset($new_columns['ansico_lifetime_views'])) { $new_columns['ansico_lifetime_views'] = __('Post Views (Lifetime)', 'ansico-stat-plugin'); $new_columns['ansico_latest_month_views'] = __('Post Views (Latest Month)', 'ansico-stat-plugin'); } return $new_columns; } public function render_view_columns($column, $post_id) { if ($column === 'ansico_lifetime_views') { echo esc_html(number_format_i18n((int) get_post_meta($post_id, self::TOTAL_META_KEY, true))); } if ($column === 'ansico_latest_month_views') { echo esc_html(number_format_i18n($this->get_post_views_for_latest_month((int) $post_id))); } } public function register_sortable_view_columns($columns) { $columns['ansico_lifetime_views'] = 'ansico_lifetime_views'; $columns['ansico_latest_month_views'] = 'ansico_latest_month_views'; return $columns; } public function render_admin_view_filter() { global $typenow, $pagenow; if (!is_admin() || $pagenow !== 'edit.php' || empty($typenow)) { return; } $post_type_object = get_post_type_object($typenow); if (!$post_type_object || empty($post_type_object->show_ui)) { return; } $selected = isset($_GET['ansico_view_filter']) ? sanitize_text_field(wp_unslash($_GET['ansico_view_filter'])) : ''; ?> is_main_query()) { return; } $view_filter = isset($_GET['ansico_view_filter']) ? sanitize_text_field(wp_unslash($_GET['ansico_view_filter'])) : ''; if ($view_filter === 'lifetime_desc' || $view_filter === 'lifetime_asc') { $query->set('meta_key', self::TOTAL_META_KEY); $query->set('orderby', 'meta_value_num'); $query->set('order', $view_filter === 'lifetime_asc' ? 'ASC' : 'DESC'); } if ($view_filter === 'month_desc' || $view_filter === 'month_asc') { $query->set('ansico_sort_latest_month_views', 1); $query->set('order', $view_filter === 'month_asc' ? 'ASC' : 'DESC'); } $orderby = $query->get('orderby'); if ($orderby === 'ansico_lifetime_views') { $query->set('meta_key', self::TOTAL_META_KEY); $query->set('orderby', 'meta_value_num'); } if ($orderby === 'ansico_latest_month_views') { $query->set('ansico_sort_latest_month_views', 1); } } public function add_monthly_views_sorting_clauses($clauses, $query) { if (!is_admin() || !$query->is_main_query() || !$query->get('ansico_sort_latest_month_views')) { return $clauses; } global $wpdb; $table = self::post_daily_table_name(); $range = $this->get_latest_month_range(); $order = strtoupper((string) $query->get('order')) === 'ASC' ? 'ASC' : 'DESC'; $join_alias = 'ansico_monthly_views'; if (strpos($clauses['join'], "AS {$join_alias}") === false) { $clauses['join'] .= $wpdb->prepare( " LEFT JOIN ( SELECT post_id, SUM(views) AS month_views FROM {$table} WHERE stat_date BETWEEN %s AND %s GROUP BY post_id ) AS {$join_alias} ON {$wpdb->posts}.ID = {$join_alias}.post_id", $range['start'], $range['end'] ); } $clauses['orderby'] = "COALESCE({$join_alias}.month_views, 0) {$order}, {$wpdb->posts}.post_date DESC"; return $clauses; } } new Ansico_Stat_Plugin(); }