diff --git a/ansico-stat-plugin/ansico-stat-plugin.php b/ansico-stat-plugin/ansico-stat-plugin.php new file mode 100644 index 0000000..e3c5a25 --- /dev/null +++ b/ansico-stat-plugin/ansico-stat-plugin.php @@ -0,0 +1,2550 @@ +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, + 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, + 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; + } + + $cookie_name = self::COOKIE_PREFIX . md5($page_context['cookie_key']); + if (isset($_COOKIE[$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); + } else { + $this->increment_non_singular_views($page_context, $referer_data); + } + + if (!headers_sent()) { + setcookie($cookie_name, '1', time() + $this->get_cookie_ttl(), COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true); + $_COOKIE[$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 = []) { + 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) + VALUES (%d, %s, 1) + ON DUPLICATE KEY UPDATE views = views + 1", + $post_id, + $today + )); + + $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 = []) { + 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) + VALUES (%s, %s, %s, %s, %s, %s, 1) + ON DUPLICATE KEY UPDATE views = views + 1, page_label = VALUES(page_label), page_url = VALUES(page_url)", + $page_key, + $page_type, + $object_id, + $label, + $url, + $today + )); + + $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)); ?>
+ 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 ?>
+
+
+
+ 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(); +} diff --git a/ansico-stat-plugin/icon-128x128.png b/ansico-stat-plugin/icon-128x128.png new file mode 100644 index 0000000..cc9e48d Binary files /dev/null and b/ansico-stat-plugin/icon-128x128.png differ diff --git a/ansico-stat-plugin/icon-256x256.png b/ansico-stat-plugin/icon-256x256.png new file mode 100644 index 0000000..215b224 Binary files /dev/null and b/ansico-stat-plugin/icon-256x256.png differ diff --git a/ansico-stat-plugin/icon.svg b/ansico-stat-plugin/icon.svg new file mode 100644 index 0000000..b35c83e --- /dev/null +++ b/ansico-stat-plugin/icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/ansico-stat-plugin/license.txt b/ansico-stat-plugin/license.txt new file mode 100644 index 0000000..5f9a26d --- /dev/null +++ b/ansico-stat-plugin/license.txt @@ -0,0 +1,2 @@ +This plugin is licensed under the GPLv2 or later. +See https://www.gnu.org/licenses/gpl-2.0.html