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();
$landing_table = self::landing_table_name();
$unique_total_table = self::unique_total_table_name();
$unique_landing_table = self::unique_landing_table_name();
$unique_page_table = self::unique_page_table_name();
$unique_referral_table = self::unique_referral_table_name();
$unique_dimension_table = self::unique_dimension_table_name();
$search_query_table = self::search_query_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,
landing_page_key varchar(191) NULL,
landing_page_label varchar(255) NULL,
landing_page_url text 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), landing_page_key),
KEY stat_date (stat_date),
KEY category (category),
KEY source_host (source_host),
KEY landing_page_key (landing_page_key(191))
) {$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};";
$sql_landing = "CREATE TABLE {$landing_table} (
stat_date date NOT NULL,
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,
visits bigint(20) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (stat_date, page_key),
KEY stat_date (stat_date),
KEY page_type (page_type),
KEY object_id (object_id),
KEY visits (visits)
) {$charset_collate};";
$sql_unique_total = "CREATE TABLE {$unique_total_table} (
period_type varchar(10) NOT NULL,
period_key varchar(16) NOT NULL,
visitor_hash varchar(64) NOT NULL,
first_seen datetime NOT NULL,
PRIMARY KEY (period_type, period_key, visitor_hash),
KEY period_lookup (period_type, period_key)
) {$charset_collate};";
$sql_unique_page = "CREATE TABLE {$unique_page_table} (
period_type varchar(10) NOT NULL,
period_key varchar(16) NOT NULL,
page_key varchar(191) NOT NULL,
visitor_hash varchar(64) NOT NULL,
first_seen datetime NOT NULL,
PRIMARY KEY (period_type, period_key, page_key, visitor_hash),
KEY period_page_lookup (period_type, period_key, page_key)
) {$charset_collate};";
$sql_unique_landing = "CREATE TABLE {$unique_landing_table} (
period_type varchar(10) NOT NULL,
period_key varchar(16) NOT NULL,
page_key varchar(191) NOT NULL,
visitor_hash varchar(64) NOT NULL,
first_seen datetime NOT NULL,
PRIMARY KEY (period_type, period_key, page_key, visitor_hash),
KEY period_landing_lookup (period_type, period_key, page_key)
) {$charset_collate};";
$sql_unique_referral = "CREATE TABLE {$unique_referral_table} (
period_type varchar(10) NOT NULL,
period_key varchar(16) NOT NULL,
category varchar(32) NOT NULL,
source_host varchar(191) NOT NULL DEFAULT '',
source_url varchar(191) NOT NULL DEFAULT '',
landing_page_key varchar(191) NOT NULL DEFAULT '',
visitor_hash varchar(64) NOT NULL,
first_seen datetime NOT NULL,
PRIMARY KEY (period_type, period_key, category, source_host, source_url, landing_page_key, visitor_hash),
KEY period_referral_lookup (period_type, period_key, category)
) {$charset_collate};";
$sql_unique_dimension = "CREATE TABLE {$unique_dimension_table} (
period_type varchar(10) NOT NULL,
period_key varchar(16) NOT NULL,
dimension_type varchar(32) NOT NULL,
dimension_value varchar(191) NOT NULL,
visitor_hash varchar(64) NOT NULL,
first_seen datetime NOT NULL,
PRIMARY KEY (period_type, period_key, dimension_type, dimension_value, visitor_hash),
KEY period_dimension_lookup (period_type, period_key, dimension_type, dimension_value)
) {$charset_collate};";
$sql_search_queries = "CREATE TABLE {$search_query_table} (
month_key char(7) NOT NULL,
property_url varchar(255) NOT NULL,
query_text varchar(255) NOT NULL,
clicks bigint(20) unsigned NOT NULL DEFAULT 0,
impressions bigint(20) unsigned NOT NULL DEFAULT 0,
ctr decimal(8,6) NOT NULL DEFAULT 0,
position decimal(10,4) NOT NULL DEFAULT 0,
updated_at datetime NOT NULL,
PRIMARY KEY (month_key, property_url, query_text),
KEY property_month_lookup (property_url, month_key),
KEY clicks (clicks),
KEY impressions (impressions)
) {$charset_collate};";
dbDelta($sql_daily);
dbDelta($sql_post_daily);
dbDelta($sql_page_daily);
dbDelta($sql_referral);
dbDelta($sql_dimensions);
dbDelta($sql_landing);
dbDelta($sql_unique_total);
dbDelta($sql_unique_page);
dbDelta($sql_unique_landing);
dbDelta($sql_unique_referral);
dbDelta($sql_unique_dimension);
dbDelta($sql_search_queries);
self::ensure_referral_table_schema($referral_table, $charset_collate);
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.
}
protected static function ensure_referral_table_schema($referral_table, $charset_collate) {
global $wpdb;
$columns = $wpdb->get_col("SHOW COLUMNS FROM {$referral_table}", 0);
if (!is_array($columns)) {
return;
}
if (!in_array('landing_page_key', $columns, true)) {
$wpdb->query("ALTER TABLE {$referral_table} ADD COLUMN landing_page_key varchar(191) NULL AFTER source_host");
}
if (!in_array('landing_page_label', $columns, true)) {
$wpdb->query("ALTER TABLE {$referral_table} ADD COLUMN landing_page_label varchar(255) NULL AFTER landing_page_key");
}
if (!in_array('landing_page_url', $columns, true)) {
$wpdb->query("ALTER TABLE {$referral_table} ADD COLUMN landing_page_url text NULL AFTER landing_page_label");
}
$indexes = $wpdb->get_results("SHOW INDEX FROM {$referral_table}", ARRAY_A);
$has_landing_index = false;
$has_unique_referral = false;
if (is_array($indexes)) {
foreach ($indexes as $index) {
$key_name = (string) ($index['Key_name'] ?? '');
$column_name = (string) ($index['Column_name'] ?? '');
if ($key_name === 'unique_referral') {
$has_unique_referral = true;
if ($column_name === 'landing_page_key') {
$has_landing_index = true;
}
}
}
}
if ($has_unique_referral && !$has_landing_index) {
$wpdb->query("ALTER TABLE {$referral_table} DROP INDEX unique_referral");
$has_unique_referral = false;
}
if (!$has_unique_referral) {
$wpdb->query("ALTER TABLE {$referral_table} ADD UNIQUE KEY unique_referral (stat_date, category, source_host(191), source_url(191), landing_page_key)");
}
$landing_key_index_exists = false;
if (is_array($indexes)) {
foreach ($indexes as $index) {
if ((string) ($index['Key_name'] ?? '') === 'landing_page_key') {
$landing_key_index_exists = true;
break;
}
}
}
if (!$landing_key_index_exists) {
$wpdb->query("ALTER TABLE {$referral_table} ADD KEY landing_page_key (landing_page_key(191))");
}
}
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';
}
public static function landing_table_name() {
global $wpdb;
return $wpdb->prefix . 'ansico_stat_landing_daily';
}
public static function unique_total_table_name() {
global $wpdb;
return $wpdb->prefix . 'ansico_stat_unique_total';
}
public static function unique_page_table_name() {
global $wpdb;
return $wpdb->prefix . 'ansico_stat_unique_page';
}
public static function unique_landing_table_name() {
global $wpdb;
return $wpdb->prefix . 'ansico_stat_unique_landing';
}
public static function unique_referral_table_name() {
global $wpdb;
return $wpdb->prefix . 'ansico_stat_unique_referral';
}
public static function unique_dimension_table_name() {
global $wpdb;
return $wpdb->prefix . 'ansico_stat_unique_dimension';
}
public static function search_query_table_name() {
global $wpdb;
return $wpdb->prefix . 'ansico_stat_search_queries';
}
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' => 0,
'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,
'google_client_id' => '',
'google_client_secret' => '',
'google_property' => '',
'google_access_token' => '',
'google_refresh_token' => '',
'google_token_expires_at' => 0,
'google_connected_email' => '',
'google_site_verification_token' => '',
'google_site_verification_method' => 'META',
'google_last_sync_month' => '',
'ipinfo_token' => '',
]);
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';
}
$settings['google_client_id'] = sanitize_text_field((string) $settings['google_client_id']);
$settings['google_client_secret'] = sanitize_text_field((string) $settings['google_client_secret']);
$settings['google_property'] = sanitize_text_field((string) $settings['google_property']);
$settings['google_access_token'] = sanitize_text_field((string) $settings['google_access_token']);
$settings['google_refresh_token'] = sanitize_text_field((string) $settings['google_refresh_token']);
$settings['google_token_expires_at'] = max(0, (int) $settings['google_token_expires_at']);
$settings['google_connected_email'] = sanitize_email((string) $settings['google_connected_email']);
$settings['google_site_verification_token'] = sanitize_text_field((string) $settings['google_site_verification_token']);
$settings['google_site_verification_method'] = strtoupper(sanitize_key((string) $settings['google_site_verification_method']));
if (!in_array($settings['google_site_verification_method'], ['META', 'DNS_TXT'], true)) {
$settings['google_site_verification_method'] = 'META';
}
$settings['google_last_sync_month'] = sanitize_text_field((string) $settings['google_last_sync_month']);
$settings['ipinfo_token'] = sanitize_text_field((string) $settings['ipinfo_token']);
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_client_ip_address() {
$keys = [
'HTTP_CF_CONNECTING_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_CLIENT_IP',
'REMOTE_ADDR',
];
foreach ($keys as $key) {
if (empty($_SERVER[$key])) {
continue;
}
$raw_value = sanitize_text_field((string) wp_unslash($_SERVER[$key]));
$parts = array_map('trim', explode(',', $raw_value));
foreach ($parts as $part) {
$ip = filter_var($part, FILTER_VALIDATE_IP);
if ($ip) {
return $ip;
}
}
}
return '';
}
protected function lookup_country_code_via_ipinfo(string $ip_address) {
$settings = $this->get_settings();
$token = trim((string) ($settings['ipinfo_token'] ?? ''));
if ($token === '' || $ip_address === '' || !filter_var($ip_address, FILTER_VALIDATE_IP)) {
return '';
}
if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
return '';
}
$cache_key = 'ansico_country_' . md5($ip_address);
$cached = get_transient($cache_key);
if (is_string($cached) && preg_match('/^[A-Z]{2}$/', $cached)) {
return $cached;
}
$response = wp_remote_get(
'https://api.ipinfo.io/lite/' . rawurlencode($ip_address) . '?token=' . rawurlencode($token),
[
'timeout' => 5,
'user-agent' => 'Ansico Stat Plugin/' . self::VERSION . '; ' . home_url('/'),
'headers' => [
'Accept' => 'application/json',
],
]
);
if (is_wp_error($response)) {
return '';
}
$code = (int) wp_remote_retrieve_response_code($response);
if ($code < 200 || $code >= 300) {
return '';
}
$data = json_decode((string) wp_remote_retrieve_body($response), true);
if (!is_array($data)) {
return '';
}
$country_code = strtoupper(sanitize_text_field((string) ($data['country_code'] ?? '')));
if (!preg_match('/^[A-Z]{2}$/', $country_code)) {
return '';
}
set_transient($cache_key, $country_code, DAY_IN_SECONDS * 30);
return $country_code;
}
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;
}
}
$ipinfo_code = $this->lookup_country_code_via_ipinfo($this->get_client_ip_address());
if ($ipinfo_code !== '') {
return $ipinfo_code;
}
return 'Unknown';
}
protected function get_country_flag_emoji(string $country_code) {
$country_code = strtoupper(trim($country_code));
if (!preg_match('/^[A-Z]{2}$/', $country_code)) {
return '';
}
$base = 127397;
$first = mb_chr($base + ord($country_code[0]), 'UTF-8');
$second = mb_chr($base + ord($country_code[1]), 'UTF-8');
if (!is_string($first) || !is_string($second)) {
return '';
}
return $first . $second;
}
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');
}
$label = '';
if (function_exists('locale_get_display_region')) {
$label = locale_get_display_region('-' . $country_code, get_locale());
if (!is_string($label)) {
$label = '';
}
}
if ($label === '') {
$label = $country_code;
}
$flag = $this->get_country_flag_emoji($country_code);
return $flag !== '' ? $flag . ' ' . $label : $label;
}
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 get_current_dimension_values() {
$dimensions = [
'country' => $this->get_country_code(),
'device' => $this->get_device_type(),
'browser' => $this->get_browser_name(),
'os' => $this->get_os_name(),
];
if (is_search()) {
$search_query = trim((string) get_search_query());
$dimensions['internal_search'] = $search_query !== '' ? $search_query : __('(empty search)', 'ansico-stat-plugin');
}
return $dimensions;
}
protected function get_period_keys(string $today) {
return [
'day' => $today,
'month' => substr($today, 0, 7),
'year' => substr($today, 0, 4),
];
}
protected function get_or_create_visitor_hash() {
$cookie_name = self::VISITOR_COOKIE_NAME;
$existing = isset($_COOKIE[$cookie_name]) ? sanitize_text_field((string) wp_unslash($_COOKIE[$cookie_name])) : '';
if (preg_match('/^[A-Za-z0-9_-]{12,64}$/', $existing)) {
return $existing;
}
$visitor_hash = wp_generate_uuid4();
$visitor_hash = str_replace('-', '', $visitor_hash);
if (!headers_sent()) {
setcookie($cookie_name, $visitor_hash, time() + self::VISITOR_COOKIE_TTL, COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true);
$_COOKIE[$cookie_name] = $visitor_hash;
}
return $visitor_hash;
}
protected function is_new_session_visit() {
$cookie_name = self::SESSION_COOKIE_NAME;
$existing = isset($_COOKIE[$cookie_name]) ? sanitize_text_field((string) wp_unslash($_COOKIE[$cookie_name])) : '';
$is_new = ($existing === '');
if (!headers_sent()) {
setcookie($cookie_name, '1', time() + $this->get_cookie_ttl(), COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true);
$_COOKIE[$cookie_name] = '1';
}
return $is_new;
}
protected function register_landing_visit(string $today, string $visitor_hash, array $page_context) {
global $wpdb;
$page_key = substr(sanitize_text_field((string) ($page_context['page_key'] ?? '')), 0, 191);
if ($page_key === '') {
return;
}
$landing_table = self::landing_table_name();
$page_type = substr(sanitize_key((string) ($page_context['page_type'] ?? 'archive')), 0, 32);
$object_id = isset($page_context['object_id']) ? (int) $page_context['object_id'] : 0;
$page_label = sanitize_text_field((string) ($page_context['label'] ?? ''));
$page_url = isset($page_context['url']) ? esc_url_raw((string) $page_context['url']) : '';
$wpdb->query($wpdb->prepare(
"INSERT INTO {$landing_table} (stat_date, page_key, page_type, object_id, page_label, page_url, visits)
VALUES (%s, %s, %s, %d, %s, %s, 1)
ON DUPLICATE KEY UPDATE visits = visits + 1, page_label = VALUES(page_label), page_url = VALUES(page_url)",
$today,
$page_key,
$page_type,
$object_id,
$page_label,
$page_url
));
if ($visitor_hash === '') {
return;
}
$unique_landing_table = self::unique_landing_table_name();
$timestamp = current_time('mysql');
foreach ($this->get_period_keys($today) as $period_type => $period_key) {
$wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO {$unique_landing_table} (period_type, period_key, page_key, visitor_hash, first_seen) VALUES (%s, %s, %s, %s, %s)",
$period_type,
$period_key,
$page_key,
$visitor_hash,
$timestamp
));
}
}
protected function register_unique_visitor(string $today, string $visitor_hash, array $page_context, array $referer_data, array $dimensions) {
global $wpdb;
if ($visitor_hash === '' || empty($page_context['page_key'])) {
return;
}
$periods = $this->get_period_keys($today);
$timestamp = current_time('mysql');
$page_key = substr(sanitize_text_field((string) $page_context['page_key']), 0, 191);
$category = substr(sanitize_key((string) ($referer_data['category'] ?? 'direct')), 0, 32);
$source_host = substr(sanitize_text_field((string) ($referer_data['source_host'] ?? '')), 0, 191);
$source_url = substr(esc_url_raw((string) ($referer_data['source_url'] ?? '')), 0, 191);
$landing_page_key = substr(sanitize_text_field((string) ($page_context['page_key'] ?? '')), 0, 191);
$unique_total_table = self::unique_total_table_name();
$unique_page_table = self::unique_page_table_name();
$unique_referral_table = self::unique_referral_table_name();
$unique_dimension_table = self::unique_dimension_table_name();
foreach ($periods as $period_type => $period_key) {
$wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO {$unique_total_table} (period_type, period_key, visitor_hash, first_seen) VALUES (%s, %s, %s, %s)",
$period_type,
$period_key,
$visitor_hash,
$timestamp
));
$wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO {$unique_page_table} (period_type, period_key, page_key, visitor_hash, first_seen) VALUES (%s, %s, %s, %s, %s)",
$period_type,
$period_key,
$page_key,
$visitor_hash,
$timestamp
));
if ($period_type === 'day' && (int) $wpdb->rows_affected > 0) {
if (($page_context['kind'] ?? '') === 'singular' && !empty($page_context['post_id'])) {
$post_daily_table = self::post_daily_table_name();
$wpdb->query($wpdb->prepare(
"UPDATE {$post_daily_table} SET unique_visitors = unique_visitors + 1 WHERE post_id = %d AND stat_date = %s",
(int) $page_context['post_id'],
$today
));
} elseif (!empty($page_context['page_key'])) {
$page_daily_table = self::page_daily_table_name();
$wpdb->query($wpdb->prepare(
"UPDATE {$page_daily_table} SET unique_visitors = unique_visitors + 1 WHERE page_key = %s AND stat_date = %s",
$page_key,
$today
));
}
}
$wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO {$unique_referral_table} (period_type, period_key, category, source_host, source_url, landing_page_key, visitor_hash, first_seen) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
$period_type,
$period_key,
$category,
$source_host,
$source_url,
$landing_page_key,
$visitor_hash,
$timestamp
));
foreach ($dimensions as $dimension_type => $dimension_value) {
$dimension_type = substr(sanitize_key((string) $dimension_type), 0, 32);
$dimension_value = substr(sanitize_text_field((string) $dimension_value), 0, 191);
if ($dimension_type === '') {
continue;
}
if ($dimension_value === '') {
$dimension_value = 'Unknown';
}
$wpdb->query($wpdb->prepare(
"INSERT IGNORE INTO {$unique_dimension_table} (period_type, period_key, dimension_type, dimension_value, visitor_hash, first_seen) VALUES (%s, %s, %s, %s, %s, %s)",
$period_type,
$period_key,
$dimension_type,
$dimension_value,
$visitor_hash,
$timestamp
));
}
}
}
protected function increment_dimension_views(string $today) {
global $wpdb;
$table = self::dimension_table_name();
$dimensions = $this->get_current_dimension_values();
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_landing_page_data(array $page_context) {
$landing_page_key = substr(sanitize_text_field((string) ($page_context['page_key'] ?? '')), 0, 191);
$landing_page_label = sanitize_text_field((string) ($page_context['label'] ?? ''));
$landing_page_url = isset($page_context['url']) ? esc_url_raw((string) $page_context['url']) : '';
return [
'landing_page_key' => $landing_page_key,
'landing_page_label' => $landing_page_label,
'landing_page_url' => $landing_page_url,
];
}
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();
$visitor_hash = $this->get_or_create_visitor_hash();
if ($this->is_new_session_visit()) {
$this->register_landing_visit(current_time('Y-m-d'), $visitor_hash, $page_context);
}
if ($page_context['kind'] === 'singular') {
$this->increment_post_views((int) $page_context['post_id'], $page_context, $referer_data, $visitor_hash);
} else {
$this->increment_non_singular_views($page_context, $referer_data, $visitor_hash);
}
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 $page_context = [], array $referer_data = [], string $visitor_hash = '') {
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']) : '';
$landing_page_data = $this->get_landing_page_data($page_context);
$wpdb->query($wpdb->prepare(
"INSERT INTO {$referral_table} (stat_date, category, source_url, source_host, landing_page_key, landing_page_label, landing_page_url, visits)
VALUES (%s, %s, %s, %s, %s, %s, %s, 1)
ON DUPLICATE KEY UPDATE visits = visits + 1, landing_page_label = VALUES(landing_page_label), landing_page_url = VALUES(landing_page_url)",
$today,
$category,
$source_url,
$source_host,
$landing_page_data['landing_page_key'],
$landing_page_data['landing_page_label'],
$landing_page_data['landing_page_url']
));
$this->register_unique_visitor($today, $visitor_hash, $page_context, $referer_data, $this->get_current_dimension_values());
$this->increment_dimension_views($today);
}
protected function increment_non_singular_views(array $page_context, array $referer_data = [], string $visitor_hash = '') {
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']) : '';
$landing_page_data = $this->get_landing_page_data($page_context);
$wpdb->query($wpdb->prepare(
"INSERT INTO {$referral_table} (stat_date, category, source_url, source_host, landing_page_key, landing_page_label, landing_page_url, visits)
VALUES (%s, %s, %s, %s, %s, %s, %s, 1)
ON DUPLICATE KEY UPDATE visits = visits + 1, landing_page_label = VALUES(landing_page_label), landing_page_url = VALUES(landing_page_url)",
$today,
$category,
$source_url,
$source_host,
$landing_page_data['landing_page_key'],
$landing_page_data['landing_page_label'],
$landing_page_data['landing_page_url']
));
$this->register_unique_visitor($today, $visitor_hash, $page_context, $referer_data, $this->get_current_dimension_values());
$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 $this->format_url_display_link($url, $url);
}
protected function get_truncated_url_text(string $text, int $limit = 100) {
$text = trim($text);
if ($text === '') {
return $text;
}
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
if (mb_strlen($text) <= $limit) {
return $text;
}
return rtrim(mb_substr($text, 0, $limit)) . '...';
}
if (strlen($text) <= $limit) {
return $text;
}
return rtrim(substr($text, 0, $limit)) . '...';
}
protected function format_url_display_link(string $url, string $label = '') {
$url = trim($url);
$label = trim($label);
if ($url === '' && $label === '') {
return '—';
}
if ($label === '') {
$label = $url;
}
$display_text = $this->get_truncated_url_text($label, 100);
$copy_label = esc_attr__('Copy URL', 'ansico-stat-plugin');
$copied_label = esc_attr__('Copied', 'ansico-stat-plugin');
if ($url === '') {
return sprintf('%2$s ', esc_attr($label), esc_html($display_text));
}
return sprintf(
'%3$s ',
esc_url($url),
esc_attr($label),
esc_html($display_text),
esc_attr($url),
$copy_label,
$copied_label
);
}
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 '';
}
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();
?>
'label',
'x_fallback_key' => 'date',
'views_label' => __('Views', 'ansico-stat-plugin'),
'unique_label' => __('Unique visitors', 'ansico-stat-plugin'),
'aria_label' => __('Views and unique visitors chart', 'ansico-stat-plugin'),
'height' => 240,
'width' => 860,
'chart_top' => 18,
'chart_bottom' => 190,
'chart_left' => 34,
'chart_right' => 830,
'x_label_y' => 216,
'tick_count' => 5,
'max_x_labels' => 8,
]);
if (empty($chart_data)) {
return '' . esc_html__('No tracked data for this period.', 'ansico-stat-plugin') . '
';
}
$labels = [];
$full_labels = [];
$views = [];
$unique = [];
foreach ($chart_data as $row) {
$label = '';
if (!empty($row[$args['label_key']])) {
$label = (string) $row[$args['label_key']];
} elseif (!empty($row[$args['x_fallback_key']])) {
$label = (string) $row[$args['x_fallback_key']];
}
$full_label = '';
if (!empty($row['tooltip_label'])) {
$full_label = (string) $row['tooltip_label'];
} elseif (!empty($row[$args['x_fallback_key']])) {
$full_label = (string) $row[$args['x_fallback_key']];
} else {
$full_label = $label;
}
$labels[] = $label;
$full_labels[] = $full_label;
$views[] = (int) ($row['views'] ?? $row['total_views'] ?? 0);
$unique[] = (int) ($row['unique_visitors'] ?? $row['total_unique'] ?? 0);
}
$max = max(array_merge($views, $unique, [0]));
if ($max <= 0) {
return '' . esc_html__('No tracked data for this period.', 'ansico-stat-plugin') . '
';
}
$height = (int) $args['height'];
$width = (int) $args['width'];
$chart_top = (float) $args['chart_top'];
$chart_bottom = (float) $args['chart_bottom'];
$chart_left = (float) $args['chart_left'];
$chart_right = (float) $args['chart_right'];
$count = count($labels);
$step_x = $count > 1 ? ($chart_right - $chart_left) / ($count - 1) : 0;
$build_series = function(array $values, string $series_key, string $value_label) use ($count, $labels, $full_labels, $step_x, $chart_left, $chart_right, $chart_bottom, $chart_top, $max) {
$poly_points = [];
$circles = [];
foreach ($values as $index => $value) {
$x = $count > 1 ? $chart_left + ($step_x * $index) : (($chart_left + $chart_right) / 2);
$y = $chart_bottom;
if ($max > 0) {
$y = $chart_bottom - (($value / $max) * ($chart_bottom - $chart_top));
}
$x = round($x, 2);
$y = round($y, 2);
$poly_points[] = $x . ',' . $y;
$circles[] = [
'x' => $x,
'y' => $y,
'label' => (string) ($labels[$index] ?? ''),
'tooltip_label' => (string) ($full_labels[$index] ?? ($labels[$index] ?? '')),
'value' => (int) $value,
'series_key' => $series_key,
'series_label' => $value_label,
];
}
return [
'points' => implode(' ', $poly_points),
'circles' => $circles,
];
};
$view_series = $build_series($views, 'views', (string) $args['views_label']);
$unique_series = $build_series($unique, 'unique', (string) $args['unique_label']);
$y_ticks = [];
$tick_count = max(2, (int) $args['tick_count']);
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();
?>
$label) :
if ($index % $label_every !== 0 && $index !== ($count - 1)) {
continue;
}
$x = $count > 1 ? $chart_left + ($step_x * $index) : (($chart_left + $chart_right) / 2);
?>
'vertical',
]);
$orientation = $args['orientation'] === 'horizontal' ? 'horizontal' : 'vertical';
$values = [];
foreach ($chart_data as $row) {
$values[] = (int) ($row['views'] ?? $row['total_views'] ?? 0);
}
$max = max($values ?: [0]);
$sum = array_sum($values);
$item_count = count($chart_data);
$slot_width = 52;
$bar_width = 28;
$row_height = 26;
if ($item_count >= 10) {
$slot_width = 40;
$bar_width = 18;
$row_height = 22;
} elseif ($item_count >= 8) {
$slot_width = 44;
$bar_width = 20;
$row_height = 24;
} elseif ($item_count >= 6) {
$slot_width = 48;
$bar_width = 24;
$row_height = 25;
}
ob_start();
?>
0 && $value > 0) ? max(2, (int) round(($value / $max) * 100)) : 0;
?>
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);
$unique_rows = $wpdb->get_results($wpdb->prepare(
"SELECT period_key, COUNT(*) AS total_unique FROM {$unique_table} WHERE period_type = %s AND period_key BETWEEN %s AND %s GROUP BY period_key ORDER BY period_key ASC",
'day',
$start,
$end
), ARRAY_A);
$indexed = [];
foreach ($rows as $row) {
$indexed[$row['stat_date']] = (int) $row['views'];
}
$unique_indexed = [];
foreach ($unique_rows as $row) {
$unique_indexed[(string) ($row['period_key'] ?? '')] = (int) ($row['total_unique'] ?? 0);
}
$data = [];
$cursor = strtotime($start);
$end_ts = strtotime($end);
while ($cursor <= $end_ts) {
$date = gmdate('Y-m-d', $cursor);
$data[] = [
'date' => $date,
'label' => wp_date(get_option('date_format'), strtotime($date)),
'views' => $indexed[$date] ?? 0,
'unique_visitors' => $unique_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 '';
}
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 '';
}
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));
$rows = $this->get_top_posts_in_period($start, $end, $limit);
return $this->apply_unique_counts_to_post_rows($rows, 'month', $month_key);
}
protected function get_yearly_top_posts(int $year, int $limit = 10) {
$start = sprintf('%04d-01-01', $year);
$end = sprintf('%04d-12-31', $year);
$rows = $this->get_top_posts_in_period($start, $end, $limit);
return $this->apply_unique_counts_to_post_rows($rows, 'year', (string) $year);
}
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_unique_total_count(string $period_type, string $period_key) {
global $wpdb;
$table = self::unique_total_table_name();
$count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE period_type = %s AND period_key = %s",
$period_type,
$period_key
));
return (int) $count;
}
protected function get_unique_page_counts(string $period_type, string $period_key, array $page_keys) {
global $wpdb;
$table = self::unique_page_table_name();
$page_keys = array_values(array_filter(array_map(function($value) {
return substr(sanitize_text_field((string) $value), 0, 191);
}, $page_keys)));
if (empty($page_keys)) {
return [];
}
$placeholders = implode(', ', array_fill(0, count($page_keys), '%s'));
$params = array_merge([$period_type, $period_key], $page_keys);
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT page_key, COUNT(*) AS total_unique FROM {$table} WHERE period_type = %s AND period_key = %s AND page_key IN ({$placeholders}) GROUP BY page_key",
$params
), ARRAY_A);
$map = [];
if (is_array($rows)) {
foreach ($rows as $row) {
$map[(string) ($row['page_key'] ?? '')] = (int) ($row['total_unique'] ?? 0);
}
}
return $map;
}
protected function apply_unique_counts_to_post_rows(array $rows, string $period_type, string $period_key) {
if (empty($rows)) {
return [];
}
$page_keys = [];
foreach ($rows as $row) {
$page_keys[] = 'post:' . (int) ($row['post_id'] ?? 0);
}
$map = $this->get_unique_page_counts($period_type, $period_key, $page_keys);
foreach ($rows as &$row) {
$page_key = 'post:' . (int) ($row['post_id'] ?? 0);
$row['unique_visitors'] = (int) ($map[$page_key] ?? 0);
}
unset($row);
return $rows;
}
protected function get_top_5_plus_others_segments(array $rows) {
$segments = [];
$other_total = 0;
foreach (array_values($rows) as $index => $row) {
$label = (string) ($row['dimension_label'] ?? $row['dimension_value'] ?? $row['label'] ?? '');
$value = (int) ($row['total_views'] ?? $row['views'] ?? 0);
if ($value <= 0) {
continue;
}
if ($index < 5) {
$segments[$label] = $value;
} else {
$other_total += $value;
}
}
if ($other_total > 0) {
$segments[__('Others', 'ansico-stat-plugin')] = $other_total;
}
return $segments;
}
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' => ['visits' => 0, 'unique_visitors' => 0],
'search' => ['visits' => 0, 'unique_visitors' => 0],
'social' => ['visits' => 0, 'unique_visitors' => 0],
'website' => ['visits' => 0, 'unique_visitors' => 0],
];
foreach ($rows as $row) {
$category = isset($row['category']) ? (string) $row['category'] : '';
if (isset($summary[$category])) {
$summary[$category]['visits'] = (int) $row['total_visits'];
}
}
$unique_table = self::unique_referral_table_name();
$unique_rows = $wpdb->get_results($wpdb->prepare(
"SELECT category, COUNT(*) AS total_unique
FROM {$unique_table}
WHERE period_type = %s
AND period_key = %s
GROUP BY category",
'month',
$month_key
), ARRAY_A);
if (is_array($unique_rows)) {
foreach ($unique_rows as $row) {
$category = isset($row['category']) ? (string) $row['category'] : '';
if (isset($summary[$category])) {
$summary[$category]['unique_visitors'] = (int) ($row['total_unique'] ?? 0);
}
}
}
return $summary;
}
protected function get_referral_sources_for_month(string $month_key, string $category, ?int $limit = null) {
global $wpdb;
$table = self::referral_table_name();
$start = $month_key . '-01';
$end = wp_date('Y-m-t', strtotime($start));
$unique_table = self::unique_referral_table_name();
$sql = $wpdb->prepare(
"SELECT r.source_url, r.source_host, r.landing_page_label, r.landing_page_url, SUM(r.visits) AS total_visits,
(
SELECT COUNT(*)
FROM {$unique_table} ur
WHERE ur.period_type = %s
AND ur.period_key = %s
AND ur.category = r.category
AND ur.source_url = LEFT(COALESCE(r.source_url, ''), 191)
AND ur.source_host = COALESCE(r.source_host, '')
AND ur.landing_page_key = COALESCE(r.landing_page_key, '')
) AS total_unique
FROM {$table} r
WHERE r.stat_date BETWEEN %s AND %s
AND r.category = %s
AND r.source_url <> ''
GROUP BY r.source_url, r.source_host, r.landing_page_key, r.landing_page_label, r.landing_page_url, r.category
ORDER BY total_visits DESC, source_host ASC, landing_page_label ASC",
'month',
$month_key,
$start,
$end,
$category
);
if ($limit !== null && $limit > 0) {
$sql .= $wpdb->prepare(' LIMIT %d', $limit);
}
$rows = $wpdb->get_results($sql, 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);
$rows = is_array($rows) ? $rows : [];
$unique_map = $this->get_unique_page_counts('month', $month_key, array_column($rows, 'page_key'));
foreach ($rows as &$row) {
$row['unique_visitors'] = (int) ($unique_map[(string) ($row['page_key'] ?? '')] ?? 0);
}
unset($row);
return $rows;
}
protected function get_monthly_top_landing_pages(string $month_key, int $limit = 10) {
global $wpdb;
$table = self::landing_table_name();
$unique_table = self::unique_landing_table_name();
$start = $month_key . '-01';
$end = wp_date('Y-m-t', strtotime($start));
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT l.page_key, l.page_label, l.page_url, SUM(l.visits) AS total_views,
(
SELECT COUNT(*)
FROM {$unique_table} ul
WHERE ul.period_type = %s
AND ul.period_key = %s
AND ul.page_key = l.page_key
) AS total_unique
FROM {$table} l
WHERE l.stat_date BETWEEN %s AND %s
GROUP BY l.page_key, l.page_label, l.page_url
ORDER BY total_views DESC, l.page_label ASC
LIMIT %d",
'month',
$month_key,
$start,
$end,
$limit
), ARRAY_A);
return is_array($rows) ? $rows : [];
}
protected function get_yearly_top_landing_pages(int $year, int $limit = 10) {
global $wpdb;
$table = self::landing_table_name();
$unique_table = self::unique_landing_table_name();
$start = sprintf('%04d-01-01', $year);
$end = sprintf('%04d-12-31', $year);
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT l.page_key, l.page_label, l.page_url, SUM(l.visits) AS total_views,
(
SELECT COUNT(*)
FROM {$unique_table} ul
WHERE ul.period_type = %s
AND ul.period_key = %s
AND ul.page_key = l.page_key
) AS total_unique
FROM {$table} l
WHERE l.stat_date BETWEEN %s AND %s
GROUP BY l.page_key, l.page_label, l.page_url
ORDER BY total_views DESC, l.page_label ASC
LIMIT %d",
'year',
(string) $year,
$start,
$end,
$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));
$unique_table = self::unique_dimension_table_name();
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT d.dimension_value, SUM(d.views) AS total_views,
(
SELECT COUNT(*)
FROM {$unique_table} ud
WHERE ud.period_type = %s
AND ud.period_key = %s
AND ud.dimension_type = d.dimension_type
AND ud.dimension_value = d.dimension_value
) AS total_unique
FROM {$table} d
WHERE d.stat_date BETWEEN %s AND %s
AND d.dimension_type = %s
GROUP BY d.dimension_value, d.dimension_type
ORDER BY total_views DESC, d.dimension_value ASC
LIMIT %d",
'month',
$month_key,
$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);
$unique_table = self::unique_dimension_table_name();
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT d.dimension_value, SUM(d.views) AS total_views,
(
SELECT COUNT(*)
FROM {$unique_table} ud
WHERE ud.period_type = %s
AND ud.period_key = %s
AND ud.dimension_type = d.dimension_type
AND ud.dimension_value = d.dimension_value
) AS total_unique
FROM {$table} d
WHERE d.stat_date BETWEEN %s AND %s
AND d.dimension_type = %s
GROUP BY d.dimension_value, d.dimension_type
ORDER BY total_views DESC, d.dimension_value ASC
LIMIT %d",
'year',
(string) $year,
$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' => ['visits' => 0, 'unique_visitors' => 0],
'Search' => ['visits' => 0, 'unique_visitors' => 0],
'Social' => ['visits' => 0, 'unique_visitors' => 0],
'Other websites' => ['visits' => 0, 'unique_visitors' => 0],
];
foreach ($rows as $row) {
$category = (string) ($row['category'] ?? '');
$value = (int) ($row['total_visits'] ?? 0);
if ($category === 'direct') {
$summary['Direct']['visits'] = $value;
} elseif ($category === 'search') {
$summary['Search']['visits'] = $value;
} elseif ($category === 'social') {
$summary['Social']['visits'] = $value;
} elseif ($category === 'website') {
$summary['Other websites']['visits'] = $value;
}
}
$unique_table = self::unique_referral_table_name();
$unique_rows = $wpdb->get_results($wpdb->prepare(
"SELECT category, COUNT(*) AS total_unique
FROM {$unique_table}
WHERE period_type = %s
AND period_key = %s
GROUP BY category",
'year',
(string) $year
), ARRAY_A);
if (is_array($unique_rows)) {
foreach ($unique_rows as $row) {
$category = (string) ($row['category'] ?? '');
$unique = (int) ($row['total_unique'] ?? 0);
if ($category === 'direct') {
$summary['Direct']['unique_visitors'] = $unique;
} elseif ($category === 'search') {
$summary['Search']['unique_visitors'] = $unique;
} elseif ($category === 'social') {
$summary['Social']['unique_visitors'] = $unique;
} elseif ($category === 'website') {
$summary['Other websites']['unique_visitors'] = $unique;
}
}
}
return $summary;
}
protected function get_monthly_totals_for_year(int $year) {
global $wpdb;
$table = self::daily_table_name();
$unique_table = self::unique_total_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);
$unique_rows = $wpdb->get_results($wpdb->prepare(
"SELECT period_key, COUNT(*) AS total_unique
FROM {$unique_table}
WHERE period_type = %s AND period_key BETWEEN %s AND %s
GROUP BY period_key
ORDER BY period_key ASC",
'month',
substr($start,0,7),
substr($end,0,7)
), ARRAY_A);
$map = [];
for ($month = 1; $month <= 12; $month++) {
$month_key = sprintf('%04d-%02d', $year, $month);
$map[$month_key] = [
'views' => 0,
'unique_visitors' => 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]['views'] += (int) ($row['views'] ?? 0);
}
}
}
if (is_array($unique_rows)) {
foreach ($unique_rows as $row) {
$month_key = (string) ($row['period_key'] ?? '');
if ($month_key !== '' && isset($map[$month_key])) {
$map[$month_key]['unique_visitors'] = (int) ($row['total_unique'] ?? 0);
}
}
}
$data = [];
foreach ($map as $month_key => $values) {
$data[] = [
'label' => wp_date('M', strtotime($month_key . '-01')),
'views' => (int) ($values['views'] ?? 0),
'unique_visitors' => (int) ($values['unique_visitors'] ?? 0),
];
}
return $data;
}
protected function get_lifetime_yearly_totals() {
global $wpdb;
$daily_table = self::daily_table_name();
$unique_table = self::unique_total_table_name();
$view_rows = $wpdb->get_results(
"SELECT YEAR(stat_date) AS stat_year, SUM(views) AS total_views
FROM {$daily_table}
GROUP BY YEAR(stat_date)
ORDER BY stat_year ASC",
ARRAY_A
);
$unique_rows = $wpdb->get_results($wpdb->prepare(
"SELECT period_key, COUNT(*) AS total_unique
FROM {$unique_table}
WHERE period_type = %s
GROUP BY period_key
ORDER BY period_key ASC",
'year'
), ARRAY_A);
$map = [];
if (is_array($view_rows)) {
foreach ($view_rows as $row) {
$year = (string) ($row['stat_year'] ?? '');
if ($year === '' || $year === '0') {
continue;
}
if (!isset($map[$year])) {
$map[$year] = ['label' => $year, 'views' => 0, 'unique_visitors' => 0];
}
$map[$year]['views'] = (int) ($row['total_views'] ?? 0);
}
}
if (is_array($unique_rows)) {
foreach ($unique_rows as $row) {
$year = preg_replace('/[^0-9]/', '', (string) ($row['period_key'] ?? ''));
if ($year === '') {
continue;
}
if (!isset($map[$year])) {
$map[$year] = ['label' => $year, 'views' => 0, 'unique_visitors' => 0];
}
$map[$year]['unique_visitors'] = (int) ($row['total_unique'] ?? 0);
}
}
if (empty($map)) {
return [];
}
ksort($map, SORT_NATURAL);
return array_values($map);
}
protected function render_lifetime_yearly_chart_markup(array $chart_data, string $chart_id) {
return $this->render_multi_line_chart_markup($chart_data, $chart_id, [
'label_key' => 'label',
'aria_label' => __('Lifetime views and unique visitors by year', 'ansico-stat-plugin'),
'x_label_y' => 246,
'height' => 280,
'width' => 860,
'chart_top' => 18,
'chart_bottom' => 220,
'chart_left' => 42,
'chart_right' => 830,
'max_x_labels' => 12,
]);
}
protected function render_lifetime_weekday_grouped_bar_chart(array $rows, string $chart_id) {
$labels = [];
$views = [];
$unique = [];
$max = 0;
foreach ($rows as $row) {
$label = (string) ($row['label'] ?? '');
$view_value = (int) ($row['views'] ?? 0);
$unique_value = (int) ($row['unique_visitors'] ?? 0);
$labels[] = $label;
$views[] = $view_value;
$unique[] = $unique_value;
$max = max($max, $view_value, $unique_value);
}
$group_count = count($labels);
$max = max($max, 1);
ob_start();
?>
$label) :
$view_height = $views[$index] > 0 ? max(6, (int) round(($views[$index] / $max) * 180)) : 0;
$unique_height = $unique[$index] > 0 ? max(6, (int) round(($unique[$index] / $max) * 180)) : 0;
?>
' . esc_html__('No lifetime data has been tracked yet.', 'ansico-stat-plugin') . '';
return;
}
?>
get_results(
"SELECT stat_date, SUM(views) AS total_views
FROM {$daily_table}
GROUP BY stat_date
ORDER BY stat_date ASC",
ARRAY_A
);
$unique_rows = $wpdb->get_results($wpdb->prepare(
"SELECT period_key, COUNT(*) AS total_unique
FROM {$unique_table}
WHERE period_type = %s
GROUP BY period_key
ORDER BY period_key ASC",
'day'
), ARRAY_A);
$labels = [
1 => __('Monday', 'ansico-stat-plugin'),
2 => __('Tuesday', 'ansico-stat-plugin'),
3 => __('Wednesday', 'ansico-stat-plugin'),
4 => __('Thursday', 'ansico-stat-plugin'),
5 => __('Friday', 'ansico-stat-plugin'),
6 => __('Saturday', 'ansico-stat-plugin'),
7 => __('Sunday', 'ansico-stat-plugin'),
];
$map = [];
foreach ($labels as $day_number => $label) {
$map[$day_number] = [
'label' => $label,
'views' => 0,
'unique_visitors' => 0,
];
}
if (is_array($view_rows)) {
foreach ($view_rows as $row) {
$date = (string) ($row['stat_date'] ?? '');
if ($date === '') {
continue;
}
$weekday = (int) wp_date('N', strtotime($date));
if (!isset($map[$weekday])) {
continue;
}
$map[$weekday]['views'] += (int) ($row['total_views'] ?? 0);
}
}
if (is_array($unique_rows)) {
foreach ($unique_rows as $row) {
$date = (string) ($row['period_key'] ?? '');
if ($date === '') {
continue;
}
$weekday = (int) wp_date('N', strtotime($date));
if (!isset($map[$weekday])) {
continue;
}
$map[$weekday]['unique_visitors'] += (int) ($row['total_unique'] ?? 0);
}
}
$total_views = 0;
$total_unique = 0;
foreach ($map as $row) {
$total_views += (int) $row['views'];
$total_unique += (int) $row['unique_visitors'];
}
$rows = [];
foreach ($map as $day_number => $row) {
$views = (int) $row['views'];
$unique = (int) $row['unique_visitors'];
$rows[] = [
'day_number' => $day_number,
'label' => (string) $row['label'],
'views' => $views,
'unique_visitors' => $unique,
'views_percent' => $total_views > 0 ? number_format_i18n(($views / $total_views) * 100, 1) . '%' : '0%',
'unique_percent' => $total_unique > 0 ? number_format_i18n(($unique / $total_unique) * 100, 1) . '%' : '0%',
];
}
return $rows;
}
protected function render_lifetime_weekday_distribution_table(array $rows) {
if (empty($rows)) {
echo '' . esc_html__('No weekday distribution data has been tracked yet.', 'ansico-stat-plugin') . '
';
return;
}
?>
(int) ($summary['direct'] ?? 0), 'unique_visitors' => 0];
$search = is_array($summary['search'] ?? null) ? $summary['search'] : ['visits' => (int) ($summary['search'] ?? 0), 'unique_visitors' => 0];
$social = is_array($summary['social'] ?? null) ? $summary['social'] : ['visits' => (int) ($summary['social'] ?? 0), 'unique_visitors' => 0];
$website = is_array($summary['website'] ?? null) ? $summary['website'] : ['visits' => (int) ($summary['website'] ?? 0), 'unique_visitors' => 0];
$total = (int) ($direct['visits'] ?? 0) + (int) ($search['visits'] ?? 0) + (int) ($social['visits'] ?? 0) + (int) ($website['visits'] ?? 0);
$unique_total = (int) ($direct['unique_visitors'] ?? 0) + (int) ($search['unique_visitors'] ?? 0) + (int) ($social['unique_visitors'] ?? 0) + (int) ($website['unique_visitors'] ?? 0);
?>
format_percent((int) ($direct['visits'] ?? 0), $total)); ?> format_percent((int) ($direct['unique_visitors'] ?? 0), $unique_total)); ?>
format_percent((int) ($search['visits'] ?? 0), $total)); ?> format_percent((int) ($search['unique_visitors'] ?? 0), $unique_total)); ?>
format_percent((int) ($social['visits'] ?? 0), $total)); ?> format_percent((int) ($social['unique_visitors'] ?? 0), $unique_total)); ?>
format_percent((int) ($website['visits'] ?? 0), $total)); ?> format_percent((int) ($website['unique_visitors'] ?? 0), $unique_total)); ?>
$threshold) {
$classes = trim('ansico-table-scroll-wrap ' . $extra_class);
$style = 'max-height:470px; overflow-y:auto; overflow-x:auto; display:block; width:100%;';
echo '';
return true;
}
return false;
}
protected function maybe_close_scrollable_table_wrapper(bool $opened) {
if ($opened) {
echo '
';
}
}
protected function render_dimension_table(array $rows, string $value_label, string $views_label = '') {
if ($views_label === '') {
$views_label = __('Views', 'ansico-stat-plugin');
}
if (empty($rows)) {
return;
}
$total = $this->get_rows_total($rows, 'total_views');
$unique_total = $this->get_rows_total($rows, 'total_unique');
$opened_scroll = $this->maybe_open_scrollable_table_wrapper(count($rows));
?>
$row) : ?>
(' . esc_html($value) . ')';
}
?>
format_percent($count, $total)); ?>
format_percent($unique, $unique_total)); ?>
maybe_close_scrollable_table_wrapper($opened_scroll); ?>
' . esc_html($empty_message) . '';
return;
}
$unique_total = $this->get_rows_total($rows, 'total_unique');
$opened_scroll = $this->maybe_open_scrollable_table_wrapper(count($rows));
?>
$row) : ?>
format_landing_page_link((string) ($row['page_url'] ?? ''), (string) ($row['page_label'] ?? '')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
format_percent($unique, $unique_total)); ?>
maybe_close_scrollable_table_wrapper($opened_scroll); ?>
' . esc_html__('No tracked 404 views for this month.', 'ansico-stat-plugin') . '';
return;
}
$opened_scroll = $this->maybe_open_scrollable_table_wrapper(count($rows));
?>
$row) : ?>
format_external_link((string) ($row['page_url'] ?? '')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
maybe_close_scrollable_table_wrapper($opened_scroll); ?>
' . esc_html__('No views tracked for this period.', 'ansico-stat-plugin') . '';
return;
}
$opened_scroll = $this->maybe_open_scrollable_table_wrapper(count($rows));
?>
$row) : ?>
format_post_link($post_id, $title); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
maybe_close_scrollable_table_wrapper($opened_scroll); ?>
get_truncated_url_text($label, 100));
}
return $this->format_url_display_link($url, $label);
}
protected function render_referral_sources_table(array $rows, string $label) {
echo '' . esc_html($label) . ' ';
if (empty($rows)) {
echo '' . esc_html__('No tracked referral URLs for this category in the selected month.', 'ansico-stat-plugin') . '
';
return;
}
?>
get_rows_total($rows, 'total_visits'); $unique_total = $this->get_rows_total($rows, 'total_unique'); ?>
get_settings();
return !empty($settings['google_access_token']) || !empty($settings['google_refresh_token']);
}
protected function get_google_connection_status_label() {
$settings = $this->get_settings();
if (!$this->is_google_connected()) {
return __('Not connected', 'ansico-stat-plugin');
}
if (!empty($settings['google_token_expires_at']) && (int) $settings['google_token_expires_at'] <= time()) {
return __('Connected, token refresh needed', 'ansico-stat-plugin');
}
return __('Connected', 'ansico-stat-plugin');
}
protected function get_google_site_url_prefix() {
return trailingslashit(home_url('/'));
}
protected function get_google_url_prefix_property() {
$site_url = $this->get_google_site_url_prefix();
if (!preg_match('#^https?://#i', $site_url)) {
$site_url = 'https://' . ltrim($site_url, '/');
}
return trailingslashit($site_url);
}
protected function get_google_auto_verification_property() {
$selected_property = $this->get_google_selected_property();
if ($selected_property !== '' && !$this->is_google_domain_property($selected_property)) {
return $selected_property;
}
return $this->get_google_url_prefix_property();
}
protected function get_google_selected_property() {
$settings = $this->get_settings();
$property = sanitize_text_field((string) ($settings['google_property'] ?? ''));
return $property !== '' ? $property : $this->get_google_site_url_prefix();
}
protected function is_google_domain_property(string $property = '') {
if ($property === '') {
$property = $this->get_google_selected_property();
}
return strpos($property, 'sc-domain:') === 0;
}
protected function get_google_verification_request_details(string $property = '') {
$property = $property !== '' ? $property : $this->get_google_selected_property();
if ($this->is_google_domain_property($property)) {
return [
'identifier' => substr($property, strlen('sc-domain:')),
'site_type' => 'INET_DOMAIN',
'method' => 'DNS_TXT',
];
}
return [
'identifier' => $property,
'site_type' => 'SITE',
'method' => 'META',
];
}
protected function build_google_oauth_authorization_url() {
$settings = $this->get_settings();
if (empty($settings['google_client_id']) || empty($settings['google_client_secret'])) {
return '';
}
$state = wp_generate_password(20, false, false);
set_transient('ansico_stat_google_oauth_state_' . get_current_user_id(), $state, 15 * MINUTE_IN_SECONDS);
$args = [
'client_id' => $settings['google_client_id'],
'redirect_uri' => $this->get_google_redirect_uri(),
'response_type' => 'code',
'scope' => implode(' ', $this->get_google_auth_scopes()),
'access_type' => 'offline',
'prompt' => 'consent',
'include_granted_scopes' => 'true',
'state' => $state,
];
return add_query_arg($args, 'https://accounts.google.com/o/oauth2/v2/auth');
}
protected function update_google_settings(array $changes) {
$settings = $this->get_settings();
$settings = array_merge($settings, $changes);
update_option(self::OPTION_KEY, $settings, false);
return $settings;
}
protected function clear_google_connection_data() {
$this->update_google_settings([
'google_access_token' => '',
'google_refresh_token' => '',
'google_token_expires_at' => 0,
'google_connected_email' => '',
'google_property' => '',
'google_site_verification_token' => '',
'google_last_sync_month' => '',
]);
}
protected function get_google_access_token() {
$settings = $this->get_settings();
if (!empty($settings['google_access_token']) && (!empty($settings['google_token_expires_at']) && (int) $settings['google_token_expires_at'] > time() + 60)) {
return $settings['google_access_token'];
}
if (empty($settings['google_refresh_token']) || empty($settings['google_client_id']) || empty($settings['google_client_secret'])) {
return '';
}
$response = wp_remote_post('https://oauth2.googleapis.com/token', [
'timeout' => 20,
'body' => [
'client_id' => $settings['google_client_id'],
'client_secret' => $settings['google_client_secret'],
'refresh_token' => $settings['google_refresh_token'],
'grant_type' => 'refresh_token',
],
]);
if (is_wp_error($response)) {
return '';
}
$code = (int) wp_remote_retrieve_response_code($response);
$body = json_decode((string) wp_remote_retrieve_body($response), true);
if ($code < 200 || $code >= 300 || !is_array($body) || empty($body['access_token'])) {
return '';
}
$this->update_google_settings([
'google_access_token' => sanitize_text_field((string) $body['access_token']),
'google_token_expires_at' => time() + max(60, (int) ($body['expires_in'] ?? 3600)),
]);
return sanitize_text_field((string) $body['access_token']);
}
protected function google_api_request($method, $url, $body = null, $access_token = '') {
if ($access_token === '') {
$access_token = $this->get_google_access_token();
}
if ($access_token === '') {
return new WP_Error('ansico_google_missing_token', __('Google access token is missing.', 'ansico-stat-plugin'));
}
$args = [
'method' => strtoupper($method),
'timeout' => 20,
'headers' => [
'Authorization' => 'Bearer ' . $access_token,
'Accept' => 'application/json',
],
];
if ($body !== null) {
$args['headers']['Content-Type'] = 'application/json';
$args['body'] = wp_json_encode($body);
}
$response = wp_remote_request($url, $args);
if (is_wp_error($response)) {
return $response;
}
$code = (int) wp_remote_retrieve_response_code($response);
$raw_body = (string) wp_remote_retrieve_body($response);
$decoded = $raw_body !== '' ? json_decode($raw_body, true) : [];
if ($code < 200 || $code >= 300) {
$message = __('Google API request failed.', 'ansico-stat-plugin');
if (is_array($decoded) && !empty($decoded['error']['message'])) {
$message = sanitize_text_field((string) $decoded['error']['message']);
}
return new WP_Error('ansico_google_api_error', $message, ['status' => $code, 'body' => $decoded]);
}
return is_array($decoded) ? $decoded : [];
}
protected function get_google_sites() {
$result = $this->google_api_request('GET', 'https://www.googleapis.com/webmasters/v3/sites');
if (is_wp_error($result)) {
return $result;
}
return is_array($result['siteEntry'] ?? null) ? $result['siteEntry'] : [];
}
protected function get_google_selected_site_permission() {
$site_url = $this->get_google_selected_property();
$sites = $this->get_google_sites();
if (is_wp_error($sites)) {
return '';
}
foreach ($sites as $site) {
if (($site['siteUrl'] ?? '') === $site_url) {
return sanitize_text_field((string) ($site['permissionLevel'] ?? ''));
}
}
return '';
}
protected function get_google_api_diagnostic_message($error, string $api_label) {
if (!is_wp_error($error)) {
return '';
}
$message = (string) $error->get_error_message();
$lower = strtolower($message);
if (strpos($lower, 'access not configured') !== false || strpos($lower, 'has not been used in project') !== false || strpos($lower, 'api has not been used') !== false) {
return sprintf(__('The %s is probably not enabled in Google Cloud yet.', 'ansico-stat-plugin'), $api_label);
}
if (strpos($lower, 'insufficient authentication scopes') !== false || strpos($lower, 'request had insufficient authentication scopes') !== false) {
return __('Reconnect with Google so the plugin can request the required permissions again.', 'ansico-stat-plugin');
}
if (strpos($lower, 'forbidden') !== false || strpos($lower, 'permission') !== false) {
return __('Google accepted the login, but the connected account does not currently have permission for this action.', 'ansico-stat-plugin');
}
if (strpos($lower, 'invalid client') !== false || strpos($lower, 'deleted_client') !== false) {
return __('The OAuth client ID or secret appears invalid. Recheck the credentials in Google Cloud.', 'ansico-stat-plugin');
}
if (strpos($lower, 'redirect_uri_mismatch') !== false) {
return __('The redirect URI in Google Cloud does not match the redirect URI shown in this plugin.', 'ansico-stat-plugin');
}
return $message !== '' ? $message : __('The Google API returned an unknown error.', 'ansico-stat-plugin');
}
protected function get_google_setup_diagnostics() {
$settings = $this->get_settings();
$diagnostics = [];
$diagnostics[] = [
'label' => __('OAuth client credentials', 'ansico-stat-plugin'),
'status' => (!empty($settings['google_client_id']) && !empty($settings['google_client_secret'])) ? 'success' : 'error',
'message' => (!empty($settings['google_client_id']) && !empty($settings['google_client_secret']))
? __('Client ID and client secret are saved.', 'ansico-stat-plugin')
: __('Add and save both the Google OAuth client ID and client secret first.', 'ansico-stat-plugin'),
];
if (!$this->is_google_connected()) {
$diagnostics[] = [
'label' => __('OAuth connection', 'ansico-stat-plugin'),
'status' => 'warning',
'message' => __('Not connected yet. If the Connect button fails before returning here, check the OAuth consent screen and authorized redirect URI in Google Cloud.', 'ansico-stat-plugin'),
];
$diagnostics[] = [
'label' => __('Search Console API', 'ansico-stat-plugin'),
'status' => 'warning',
'message' => __('This can be tested after you connect Google.', 'ansico-stat-plugin'),
];
$diagnostics[] = [
'label' => __('Site Verification API', 'ansico-stat-plugin'),
'status' => 'warning',
'message' => __('This can be tested after you connect Google.', 'ansico-stat-plugin'),
];
return $diagnostics;
}
$access_token = $this->get_google_access_token();
if ($access_token === '') {
$diagnostics[] = [
'label' => __('OAuth connection', 'ansico-stat-plugin'),
'status' => 'error',
'message' => __('The plugin is marked as connected, but it could not refresh the Google access token. Try reconnecting with Google.', 'ansico-stat-plugin'),
];
return $diagnostics;
}
$diagnostics[] = [
'label' => __('OAuth connection', 'ansico-stat-plugin'),
'status' => 'success',
'message' => !empty($settings['google_connected_email'])
? sprintf(__('Connected as %s.', 'ansico-stat-plugin'), $settings['google_connected_email'])
: __('Connected and the access token is working.', 'ansico-stat-plugin'),
];
$sites_result = $this->google_api_request('GET', 'https://www.googleapis.com/webmasters/v3/sites', null, $access_token);
$diagnostics[] = [
'label' => __('Search Console API', 'ansico-stat-plugin'),
'status' => is_wp_error($sites_result) ? 'error' : 'success',
'message' => is_wp_error($sites_result)
? $this->get_google_api_diagnostic_message($sites_result, __('Search Console API', 'ansico-stat-plugin'))
: __('The plugin can list Search Console properties successfully.', 'ansico-stat-plugin'),
];
$verification_result = $this->google_api_request('GET', 'https://www.googleapis.com/siteVerification/v1/webResource', null, $access_token);
$diagnostics[] = [
'label' => __('Site Verification API', 'ansico-stat-plugin'),
'status' => is_wp_error($verification_result) ? 'error' : 'success',
'message' => is_wp_error($verification_result)
? $this->get_google_api_diagnostic_message($verification_result, __('Site Verification API', 'ansico-stat-plugin'))
: __('The plugin can reach the Site Verification API successfully.', 'ansico-stat-plugin'),
];
$property = $this->get_google_selected_property();
$diagnostics[] = [
'label' => __('Selected property', 'ansico-stat-plugin'),
'status' => $property !== '' ? 'success' : 'warning',
'message' => $property !== ''
? sprintf(__('Current property: %s', 'ansico-stat-plugin'), $property)
: __('Choose a Search Console property and save the settings.', 'ansico-stat-plugin'),
];
return $diagnostics;
}
protected function render_google_setup_diagnostics() {
$diagnostics = $this->get_google_setup_diagnostics();
$status_labels = [
'success' => __('OK', 'ansico-stat-plugin'),
'warning' => __('Check', 'ansico-stat-plugin'),
'error' => __('Problem', 'ansico-stat-plugin'),
];
echo '';
echo '' . esc_html__('Check', 'ansico-stat-plugin') . ' ' . esc_html__('Status', 'ansico-stat-plugin') . ' ' . esc_html__('Details', 'ansico-stat-plugin') . ' ';
foreach ($diagnostics as $row) {
$status = (string) ($row['status'] ?? 'warning');
$label = (string) ($status_labels[$status] ?? $status_labels['warning']);
$color = $status === 'success' ? '#2271b1' : ($status === 'error' ? '#b32d2e' : '#996800');
echo '';
echo '' . esc_html((string) ($row['label'] ?? '')) . ' ';
echo '' . esc_html($label) . ' ';
echo '' . esc_html((string) ($row['message'] ?? '')) . ' ';
echo ' ';
}
echo '
';
}
protected function get_google_site_verification_status() {
$settings = $this->get_settings();
if (!$this->is_google_connected()) {
return __('Not connected', 'ansico-stat-plugin');
}
$permission = $this->get_google_selected_site_permission();
if ($permission !== '') {
return sprintf(__('Verified in Search Console (%s)', 'ansico-stat-plugin'), $permission);
}
if (!empty($settings['google_site_verification_token'])) {
if ($this->is_google_domain_property()) {
return __('DNS verification token generated, waiting for DNS verification', 'ansico-stat-plugin');
}
return __('Verification token generated, waiting for verification', 'ansico-stat-plugin');
}
return __('Not verified in Search Console yet', 'ansico-stat-plugin');
}
protected function get_google_search_console_help_text() {
return __('Use the Connect with Google button below for a Site Kit-style OAuth flow. First save your Google client ID and client secret, then connect, choose a property, verify ownership, and sync the selected month of search queries.', 'ansico-stat-plugin');
}
protected function get_google_connection_help_text() {
if ($this->is_google_connected()) {
return __('Google is already connected. You can reconnect to refresh permissions or disconnect and connect a different Google account.', 'ansico-stat-plugin');
}
return __('After you save the client ID and client secret, click Connect with Google. WordPress will send you to Google, and the access and refresh tokens will be stored automatically when you return.', 'ansico-stat-plugin');
}
public function render_google_site_verification_meta_tag() {
$settings = $this->get_settings();
if (empty($settings['google_site_verification_token']) || is_admin()) {
return;
}
echo "
" . ' ' . "
";
}
public function handle_google_connect() {
if (!current_user_can('manage_options')) {
wp_die(esc_html__('You do not have permission to connect Google Search Console.', 'ansico-stat-plugin'));
}
check_admin_referer('ansico_stat_google_connect');
$url = $this->build_google_oauth_authorization_url();
if ($url === '') {
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=missing-credentials'));
exit;
}
wp_safe_redirect($url);
exit;
}
public function handle_google_disconnect() {
if (!current_user_can('manage_options')) {
wp_die(esc_html__('You do not have permission to disconnect Google Search Console.', 'ansico-stat-plugin'));
}
check_admin_referer('ansico_stat_google_disconnect');
$this->clear_google_connection_data();
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=disconnected'));
exit;
}
public function handle_google_oauth_callback() {
if (!current_user_can('manage_options')) {
wp_die(esc_html__('You do not have permission to complete Google authentication.', 'ansico-stat-plugin'));
}
$state = isset($_GET['state']) ? sanitize_text_field(wp_unslash($_GET['state'])) : '';
$saved_state = get_transient('ansico_stat_google_oauth_state_' . get_current_user_id());
delete_transient('ansico_stat_google_oauth_state_' . get_current_user_id());
if ($state === '' || !$saved_state || !hash_equals((string) $saved_state, $state)) {
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=invalid-state'));
exit;
}
if (!empty($_GET['error'])) {
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=oauth-denied'));
exit;
}
$code = isset($_GET['code']) ? sanitize_text_field(wp_unslash($_GET['code'])) : '';
$settings = $this->get_settings();
if ($code === '' || empty($settings['google_client_id']) || empty($settings['google_client_secret'])) {
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=oauth-failed'));
exit;
}
$response = wp_remote_post('https://oauth2.googleapis.com/token', [
'timeout' => 20,
'body' => [
'code' => $code,
'client_id' => $settings['google_client_id'],
'client_secret' => $settings['google_client_secret'],
'redirect_uri' => $this->get_google_redirect_uri(),
'grant_type' => 'authorization_code',
],
]);
if (is_wp_error($response)) {
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=oauth-failed'));
exit;
}
$body = json_decode((string) wp_remote_retrieve_body($response), true);
if ((int) wp_remote_retrieve_response_code($response) < 200 || (int) wp_remote_retrieve_response_code($response) >= 300 || !is_array($body) || empty($body['access_token'])) {
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=oauth-failed'));
exit;
}
$email = '';
if (!empty($body['id_token'])) {
$parts = explode('.', (string) $body['id_token']);
if (count($parts) === 3) {
$payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true);
if (is_array($payload) && !empty($payload['email'])) {
$email = sanitize_email((string) $payload['email']);
}
}
}
$this->update_google_settings([
'google_access_token' => sanitize_text_field((string) $body['access_token']),
'google_refresh_token' => !empty($body['refresh_token']) ? sanitize_text_field((string) $body['refresh_token']) : $settings['google_refresh_token'],
'google_token_expires_at' => time() + max(60, (int) ($body['expires_in'] ?? 3600)),
'google_connected_email' => $email,
]);
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=connected'));
exit;
}
public function handle_google_generate_verification_token() {
if (!current_user_can('manage_options')) {
wp_die(esc_html__('You do not have permission to generate a Google verification token.', 'ansico-stat-plugin'));
}
check_admin_referer('ansico_stat_google_generate_verification_token');
$verification_property = $this->get_google_auto_verification_property();
$details = $this->get_google_verification_request_details($verification_property);
$result = $this->google_api_request('POST', 'https://www.googleapis.com/siteVerification/v1/token', [
'site' => [
'type' => $details['site_type'],
'identifier' => $details['identifier'],
],
'verificationMethod' => $details['method'],
]);
if (is_wp_error($result) || empty($result['token'])) {
$message = is_wp_error($result) ? $result->get_error_message() : __('Could not generate a Google site verification token.', 'ansico-stat-plugin');
set_transient('ansico_stat_google_notice_' . get_current_user_id(), [
'type' => 'error',
'message' => $message,
], MINUTE_IN_SECONDS * 5);
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=token-failed'));
exit;
}
$this->update_google_settings([
'google_site_verification_token' => sanitize_text_field((string) $result['token']),
'google_site_verification_method' => sanitize_key((string) $details['method']),
]);
set_transient('ansico_stat_google_notice_' . get_current_user_id(), [
'type' => 'success',
'message' => __('Google site verification token generated for the URL-prefix property.', 'ansico-stat-plugin'),
], MINUTE_IN_SECONDS * 5);
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=token-generated'));
exit;
}
public function handle_google_verify_site() {
if (!current_user_can('manage_options')) {
wp_die(esc_html__('You do not have permission to verify the site in Google Search Console.', 'ansico-stat-plugin'));
}
check_admin_referer('ansico_stat_google_verify_site');
$property = $this->get_google_auto_verification_property();
$details = $this->get_google_verification_request_details($property);
$result = $this->google_api_request('PUT', 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode($property), null);
if (is_wp_error($result) && !in_array($result->get_error_code(), ['http_409', 'http_201'], true)) {
wp_safe_redirect(add_query_arg([
'page' => 'ansico-stat-plugin',
'tab' => 'settings',
'google_status' => 'verify-failed',
'google_message' => rawurlencode($result->get_error_message()),
], admin_url('admin.php')));
exit;
}
$result = $this->google_api_request('POST', 'https://www.googleapis.com/siteVerification/v1/webResource?verificationMethod=' . rawurlencode($details['method']), [
'site' => [
'type' => $details['site_type'],
'identifier' => $details['identifier'],
],
]);
if (is_wp_error($result)) {
set_transient('ansico_stat_google_notice_' . get_current_user_id(), [
'type' => 'error',
'message' => $result->get_error_message(),
], MINUTE_IN_SECONDS * 5);
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=verify-failed'));
exit;
}
// Try to add the verified property to Search Console.
$this->google_api_request('PUT', 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode($property), null);
$this->update_google_settings([
'google_property' => $property,
]);
set_transient('ansico_stat_google_notice_' . get_current_user_id(), [
'type' => 'success',
'message' => __('Site verification completed.', 'ansico-stat-plugin'),
], MINUTE_IN_SECONDS * 5);
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&google-status=verified'));
exit;
}
protected function get_google_available_properties() {
$sites = $this->get_google_sites();
if (is_wp_error($sites)) {
return [];
}
$properties = [];
foreach ($sites as $site) {
$site_url = sanitize_text_field((string) ($site['siteUrl'] ?? ''));
if ($site_url === '') {
continue;
}
$properties[] = [
'siteUrl' => $site_url,
'permissionLevel' => sanitize_text_field((string) ($site['permissionLevel'] ?? '')),
];
}
usort($properties, function ($a, $b) {
return strcmp($a['siteUrl'], $b['siteUrl']);
});
return $properties;
}
protected function get_month_date_range(string $month_key) {
$month_key = preg_match('/^\d{4}-\d{2}$/', $month_key) ? $month_key : wp_date('Y-m', current_time('timestamp'));
$start = $month_key . '-01';
$end = wp_date('Y-m-t', strtotime($start));
return [$start, $end];
}
protected function sync_google_search_queries_for_month(string $month_key, int $row_limit = 50) {
global $wpdb;
$settings = $this->get_settings();
$property = sanitize_text_field((string) ($settings['google_property'] ?? ''));
if ($property === '') {
return new WP_Error('ansico_google_missing_property', __('No Google Search Console property has been selected yet.', 'ansico-stat-plugin'));
}
[$start_date, $end_date] = $this->get_month_date_range($month_key);
$endpoint = 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode($property) . '/searchAnalytics/query';
$result = $this->google_api_request('POST', $endpoint, [
'startDate' => $start_date,
'endDate' => $end_date,
'dimensions' => ['query'],
'rowLimit' => max(1, min(250, $row_limit)),
'dataState' => 'final',
]);
if (is_wp_error($result)) {
return $result;
}
$table = self::search_query_table_name();
$wpdb->delete($table, [
'month_key' => $month_key,
'property_url' => $property,
], ['%s', '%s']);
$rows = is_array($result['rows'] ?? null) ? $result['rows'] : [];
$timestamp = current_time('mysql');
foreach ($rows as $row) {
$query = sanitize_text_field((string) (($row['keys'][0] ?? '')));
if ($query === '') {
continue;
}
$wpdb->replace($table, [
'month_key' => $month_key,
'property_url' => $property,
'query_text' => mb_substr($query, 0, 255),
'clicks' => max(0, (int) round((float) ($row['clicks'] ?? 0))),
'impressions' => max(0, (int) round((float) ($row['impressions'] ?? 0))),
'ctr' => (float) ($row['ctr'] ?? 0),
'position' => (float) ($row['position'] ?? 0),
'updated_at' => $timestamp,
], ['%s', '%s', '%s', '%d', '%d', '%f', '%f', '%s']);
}
$this->update_google_settings([
'google_last_sync_month' => $month_key,
]);
return count($rows);
}
protected function get_google_search_queries_for_month(string $month_key, int $limit = 50) {
global $wpdb;
$settings = $this->get_settings();
$property = sanitize_text_field((string) ($settings['google_property'] ?? ''));
if ($property === '') {
return [];
}
$table = self::search_query_table_name();
return $wpdb->get_results($wpdb->prepare(
"SELECT query_text, clicks, impressions, ctr, position, updated_at
FROM {$table}
WHERE month_key = %s AND property_url = %s
ORDER BY clicks DESC, impressions DESC, query_text ASC
LIMIT %d",
$month_key,
$property,
max(1, min(250, $limit))
), ARRAY_A) ?: [];
}
protected function render_google_search_queries_table(array $rows, string $empty_message = '') {
if ($empty_message === '') {
$empty_message = __('No Google Search Console query data has been imported for this month yet.', 'ansico-stat-plugin');
}
if (empty($rows)) {
echo '' . esc_html($empty_message) . '
';
return;
}
$opened_scroll = $this->maybe_open_scrollable_table_wrapper(count($rows));
?>
maybe_close_scrollable_table_wrapper($opened_scroll); ?>
sync_google_search_queries_for_month($month);
if (is_wp_error($result)) {
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_STATS . '&month=' . rawurlencode($month) . '&google-sync=failed'));
exit;
}
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_STATS . '&month=' . rawurlencode($month) . '&google-sync=success'));
exit;
}
public function render_settings_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();
$google_properties = $this->is_google_connected() ? $this->get_google_available_properties() : [];
$google_auth_url = $this->build_google_oauth_authorization_url();
?>
get_google_search_console_help_text()); ?>
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']));
$chart_data = $this->get_daily_chart_data(60);
foreach ($chart_data as &$chart_row) {
if (!empty($chart_row['date'])) {
$timestamp = strtotime((string) $chart_row['date']);
if ($timestamp) {
$chart_row['label'] = wp_date('j', $timestamp);
$chart_row['tooltip_label'] = wp_date('j. F Y', $timestamp);
}
}
}
unset($chart_row);
$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']['visits'] ?? 0),
__('Search engines', 'ansico-stat-plugin') => (int) ($referral_summary['search']['visits'] ?? 0),
__('Social media', 'ansico-stat-plugin') => (int) ($referral_summary['social']['visits'] ?? 0),
__('Other websites', 'ansico-stat-plugin') => (int) ($referral_summary['website']['visits'] ?? 0),
];
$search_referrals = $this->get_referral_sources_for_month($selected_month, 'search');
$social_referrals = $this->get_referral_sources_for_month($selected_month, 'social');
$website_referrals = $this->get_referral_sources_for_month($selected_month, 'website');
$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);
$monthly_landing_rows = $this->get_monthly_top_landing_pages($selected_month, $top_rows);
$monthly_internal_search_rows = $this->get_dimension_rows_for_month($selected_month, 'internal_search', $top_rows);
$monthly_search_query_rows = $this->get_google_search_queries_for_month($selected_month, max(25, $top_rows));
?>
render_multi_line_chart_markup($chart_data, 'ansico-stat-admin-chart', ['aria_label' => __('Daily views and unique visitors (last 60 days)', 'ansico-stat-plugin')]); // 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_landing_pages_table($monthly_landing_rows, __('No landing pages tracked for this month yet.', 'ansico-stat-plugin')); ?>
render_month_navigation($selected_month); ?>
render_dimension_table($monthly_internal_search_rows, __('Search query', 'ansico-stat-plugin')); ?>
render_pie_chart_markup($this->get_top_5_plus_others_segments($monthly_internal_search_rows), 'ansico-monthly-internal-search-pie', __('Monthly internal search queries', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
render_month_navigation($selected_month); ?>
is_google_connected()) : ?>
render_google_search_queries_table($monthly_search_query_rows, __('No Google Search Console query data has been synced for this month yet.', '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')); ?>
render_month_navigation($selected_month); ?>
is_unknown_only_dimension_rows($monthly_country_rows)) : ?>
render_dimension_table($monthly_country_rows, __('Country', 'ansico-stat-plugin')); ?>
render_pie_chart_markup($this->get_top_5_plus_others_segments($monthly_country_rows), 'ansico-monthly-country-pie', __('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_pie_chart_markup($this->get_top_5_plus_others_segments($monthly_browser_rows), 'ansico-monthly-browser-pie', __('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_pie_chart_markup($this->get_top_5_plus_others_segments($monthly_os_rows), 'ansico-monthly-os-pie', __('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_internal_search_rows = $this->get_dimension_rows_for_year($selected_year, 'internal_search', $top_rows);
$yearly_referral_summary = $this->get_referral_summary_for_year($selected_year);
$yearly_month_chart = $this->get_monthly_totals_for_year($selected_year);
$yearly_landing_rows = $this->get_yearly_top_landing_pages($selected_year, $top_rows);
?>
render_year_navigation($selected_year); ?>
render_multi_line_chart_markup($yearly_month_chart, 'ansico-yearly-month-chart', ['aria_label' => __('Yearly views and unique visitors by month', 'ansico-stat-plugin'), 'max_x_labels' => 12]); // 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_landing_pages_table($yearly_landing_rows, __('No landing pages tracked for this year yet.', 'ansico-stat-plugin')); ?>
render_year_navigation($selected_year); ?>
render_dimension_table($yearly_internal_search_rows, __('Search query', 'ansico-stat-plugin')); ?>
render_pie_chart_markup($this->get_top_5_plus_others_segments($yearly_internal_search_rows), 'ansico-yearly-internal-search-pie', __('Yearly internal search queries', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
render_year_navigation($selected_year); ?>
render_referral_summary_table([
'direct' => [
'visits' => (int) ($yearly_referral_summary['Direct']['visits'] ?? 0),
'unique_visitors' => (int) ($yearly_referral_summary['Direct']['unique_visitors'] ?? 0),
],
'search' => [
'visits' => (int) ($yearly_referral_summary['Search']['visits'] ?? 0),
'unique_visitors' => (int) ($yearly_referral_summary['Search']['unique_visitors'] ?? 0),
],
'social' => [
'visits' => (int) ($yearly_referral_summary['Social']['visits'] ?? 0),
'unique_visitors' => (int) ($yearly_referral_summary['Social']['unique_visitors'] ?? 0),
],
'website' => [
'visits' => (int) ($yearly_referral_summary['Other websites']['visits'] ?? 0),
'unique_visitors' => (int) ($yearly_referral_summary['Other websites']['unique_visitors'] ?? 0),
],
]); ?>
render_pie_chart_markup([
__('Direct', 'ansico-stat-plugin') => (int) ($yearly_referral_summary['Direct']['visits'] ?? 0),
__('Search engines', 'ansico-stat-plugin') => (int) ($yearly_referral_summary['Search']['visits'] ?? 0),
__('Social media', 'ansico-stat-plugin') => (int) ($yearly_referral_summary['Social']['visits'] ?? 0),
__('Other websites', 'ansico-stat-plugin') => (int) ($yearly_referral_summary['Other websites']['visits'] ?? 0),
], 'ansico-yearly-referral-pie', __('Yearly referred visitors', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
render_year_navigation($selected_year); ?>
is_unknown_only_dimension_rows($yearly_country_rows)) : ?>
render_dimension_table($yearly_country_rows, __('Country', 'ansico-stat-plugin')); ?>
render_pie_chart_markup($this->get_top_5_plus_others_segments($yearly_country_rows), 'ansico-yearly-country-pie', __('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_pie_chart_markup($this->get_top_5_plus_others_segments($yearly_browser_rows), 'ansico-yearly-browser-pie', __('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_pie_chart_markup($this->get_top_5_plus_others_segments($yearly_os_rows), 'ansico-yearly-os-pie', __('Yearly operating systems', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
get_lifetime_yearly_totals();
$weekday_rows = $this->get_lifetime_weekday_distribution();
?>
render_lifetime_yearly_chart_markup($lifetime_chart, 'ansico-lifetime-yearly-chart'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
render_lifetime_yearly_table($lifetime_chart); ?>
render_lifetime_weekday_grouped_bar_chart($weekday_rows, 'ansico-lifetime-weekday-grouped-bar'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
render_lifetime_weekday_distribution_table($weekday_rows); ?>
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),
];
}
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['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')];
}
$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')];
}
$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,
'target' => [
'kind' => 'singular',
'post_id' => (int) $post_id,
'page_key' => 'post:' . (int) $post_id,
'label' => get_the_title($post_id) ?: __('(no title)', 'ansico-stat-plugin'),
'url' => get_permalink($post_id) ?: $normalized_url,
],
];
}
}
$front_url = $this->normalize_target_url(home_url('/'));
if ($normalized_url === $front_url) {
return [
'success' => true,
'target' => [
'kind' => 'archive',
'page_key' => 'front_page',
'label' => __('Front page', 'ansico-stat-plugin'),
'url' => home_url('/'),
],
];
}
$table = self::page_daily_table_name();
$variants = array_values(array_unique(array_filter([$normalized_url, untrailingslashit($normalized_url), trailingslashit($normalized_url)])));
if (!empty($variants)) {
$placeholders = implode(',', array_fill(0, count($variants), '%s'));
$row = $wpdb->get_row($wpdb->prepare("SELECT page_key, page_label, page_url FROM {$table} WHERE page_url IN ({$placeholders}) ORDER BY stat_date DESC LIMIT 1", $variants), ARRAY_A);
if (is_array($row) && !empty($row['page_key'])) {
return [
'success' => true,
'target' => [
'kind' => 'archive',
'page_key' => (string) $row['page_key'],
'label' => sanitize_text_field((string) ($row['page_label'] ?? $normalized_url)),
'url' => !empty($row['page_url']) ? esc_url_raw((string) $row['page_url']) : $normalized_url,
],
];
}
}
return ['success' => false, 'message' => __('No tracked page or post was found for that URL yet.', 'ansico-stat-plugin')];
}
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();
return (int) $wpdb->get_var($wpdb->prepare("SELECT SUM(views) FROM {$table} WHERE page_key = %s", (string) $target['page_key']));
}
protected function get_target_lifetime_unique_visitors(array $target) {
global $wpdb;
$table = self::unique_page_table_name();
return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(DISTINCT visitor_hash) FROM {$table} WHERE page_key = %s", (string) $target['page_key']));
}
protected function get_target_period_views(array $target, string $start, string $end) {
global $wpdb;
if (($target['kind'] ?? '') === 'singular') {
$table = self::post_daily_table_name();
return (int) $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));
}
$table = self::page_daily_table_name();
return (int) $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));
}
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();
return (int) $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));
}
$table = self::page_daily_table_name();
return (int) $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));
}
protected function get_target_daily_combined_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 stat_date, views, 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, views, 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']] = [
'views' => (int) ($row['views'] ?? 0),
'unique_visitors' => (int) ($row['unique_visitors'] ?? 0),
];
}
$data = [];
$cursor = strtotime($start);
$end_ts = strtotime($end);
while ($cursor <= $end_ts) {
$date = gmdate('Y-m-d', $cursor);
$data[] = [
'date' => $date,
'label' => wp_date('j M', $cursor),
'tooltip_label' => wp_date(get_option('date_format'), $cursor),
'views' => (int) ($indexed[$date]['views'] ?? 0),
'unique_visitors' => (int) ($indexed[$date]['unique_visitors'] ?? 0),
];
$cursor = strtotime('+1 day', $cursor);
}
return $data;
}
protected function get_target_monthly_combined_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, SUM(unique_visitors) AS total_unique 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, SUM(unique_visitors) AS total_unique 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']] = [
'views' => (int) ($row['total_views'] ?? 0),
'unique_visitors' => (int) ($row['total_unique'] ?? 0),
];
}
$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),
'tooltip_label' => wp_date('F Y', $cursor),
'views' => (int) ($indexed[$month_key]['views'] ?? 0),
'unique_visitors' => (int) ($indexed[$month_key]['unique_visitors'] ?? 0),
];
$cursor = strtotime('+1 month', $cursor);
}
return $data;
}
protected function get_target_weekday_breakdown_between(array $target, string $start, string $end) {
$daily = $this->get_target_daily_combined_chart_data_between($target, $start, $end);
$map = [
__('Mon', 'ansico-stat-plugin') => ['views' => 0, 'unique_visitors' => 0],
__('Tue', 'ansico-stat-plugin') => ['views' => 0, 'unique_visitors' => 0],
__('Wed', 'ansico-stat-plugin') => ['views' => 0, 'unique_visitors' => 0],
__('Thu', 'ansico-stat-plugin') => ['views' => 0, 'unique_visitors' => 0],
__('Fri', 'ansico-stat-plugin') => ['views' => 0, 'unique_visitors' => 0],
__('Sat', 'ansico-stat-plugin') => ['views' => 0, 'unique_visitors' => 0],
__('Sun', 'ansico-stat-plugin') => ['views' => 0, 'unique_visitors' => 0],
];
$keys = array_keys($map);
foreach ($daily as $row) {
$timestamp = strtotime((string) ($row['date'] ?? ''));
if (!$timestamp) {
continue;
}
$index = (int) gmdate('N', $timestamp) - 1;
if (isset($keys[$index])) {
$map[$keys[$index]]['views'] += (int) ($row['views'] ?? 0);
$map[$keys[$index]]['unique_visitors'] += (int) ($row['unique_visitors'] ?? 0);
}
}
$rows = [];
foreach ($map as $label => $vals) {
$rows[] = ['label' => $label, 'views' => (int) $vals['views'], 'unique_visitors' => (int) $vals['unique_visitors']];
}
return $rows;
}
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, unique_visitors 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, unique_visitors 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();
return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE post_id = %d", (int) $target['post_id']));
}
$table = self::page_daily_table_name();
return (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE page_key = %s", (string) $target['page_key']));
}
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;
$url = add_query_arg($args, admin_url('admin-post.php'));
return wp_nonce_url($url, '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'] ?? '0')) . '
';
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')) {
return;
}
$requested_url = $this->get_requested_single_stats_url();
$range = $this->get_single_stats_date_range();
$result = $this->resolve_single_stats_target($requested_url);
$target = !empty($result['success']) ? (array) ($result['target'] ?? []) : [];
$range_start = $range['start'];
$range_end = $range['end'];
$daily_chart = !empty($target) ? $this->get_target_daily_combined_chart_data_between($target, $range_start, $range_end) : [];
$monthly_chart = !empty($target) ? $this->get_target_monthly_combined_chart_data_between($target, $range_start, $range_end) : [];
$weekday_rows = !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 = !empty($target) ? $this->get_target_lifetime_unique_visitors($target) : 0;
$period_views = !empty($target) ? $this->get_target_period_views($target, $range_start, $range_end) : 0;
$period_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) : [];
$tracked_days = !empty($target) ? $this->get_target_total_tracked_days($target) : 0;
$first_tracked = !empty($target) ? $this->get_target_first_tracked_date($target) : '';
$export_url = !empty($target) ? $this->build_single_export_url($requested_url, $range_start, $range_end) : '';
?>
render_single_stats_summary_cards([
['label' => __('Views in selected period', 'ansico-stat-plugin'), 'value' => number_format_i18n($period_views)],
['label' => __('Unique visitors in selected period', 'ansico-stat-plugin'), 'value' => number_format_i18n($period_unique)],
['label' => __('Lifetime views', 'ansico-stat-plugin'), 'value' => number_format_i18n($lifetime_views)],
['label' => __('Lifetime unique visitors', 'ansico-stat-plugin'), 'value' => number_format_i18n($lifetime_unique)],
['label' => __('Tracked days', 'ansico-stat-plugin'), 'value' => number_format_i18n($tracked_days), 'description' => $first_tracked !== '' ? sprintf(__('Since %s', 'ansico-stat-plugin'), wp_date(get_option('date_format'), strtotime($first_tracked))) : ''],
['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'])) : ''],
]); ?>
render_multi_line_chart_markup($daily_chart, 'ansico-single-daily-chart', ['label_key' => 'label', 'x_fallback_key' => 'date', 'aria_label' => __('Views and unique visitors for selected page', 'ansico-stat-plugin'), 'max_x_labels' => 10]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
render_multi_line_chart_markup($monthly_chart, 'ansico-single-monthly-chart', ['label_key' => 'label', 'x_fallback_key' => 'label', 'aria_label' => __('Monthly totals for selected page', 'ansico-stat-plugin'), 'max_x_labels' => 12]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
render_lifetime_weekday_grouped_bar_chart($weekday_rows, 'ansico-single-weekday-chart'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
get_requested_single_stats_url();
$range = $this->get_single_stats_date_range();
$result = $this->resolve_single_stats_target($requested_url);
if (empty($result['success'])) {
wp_safe_redirect($this->build_single_stats_admin_url($requested_url, $range['start'], $range['end']));
exit;
}
$target = (array) ($result['target'] ?? []);
$rows = $this->get_target_daily_combined_chart_data_between($target, $range['start'], $range['end']);
$filename_slug = sanitize_title((string) ($target['label'] ?? 'single-page'));
if ($filename_slug === '') {
$filename_slug = 'single-page';
}
nocache_headers();
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="ansico-single-page-' . $filename_slug . '-' . wp_date('Y-m-d-H-i-s') . '.csv"');
$output = fopen('php://output', 'w');
if ($output === false) {
exit;
}
fputcsv($output, ['date', 'views', 'unique_visitors']);
foreach ($rows as $row) {
fputcsv($output, [(string) ($row['date'] ?? ''), (int) ($row['views'] ?? 0), (int) ($row['unique_visitors'] ?? 0)]);
}
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();
$existing_settings = $this->get_settings();
$settings_section = isset($_POST['settings_section']) ? sanitize_key(wp_unslash($_POST['settings_section'])) : 'tracking';
$submitted_post_types = ($settings_section === 'tracking' && isset($_POST['track_post_types']) && is_array($_POST['track_post_types'])) ? array_map('sanitize_key', wp_unslash($_POST['track_post_types'])) : $existing_settings['track_post_types'];
$submitted_post_types = array_values(array_intersect($allowed_post_types, $submitted_post_types));
$visitor_counting_mode = ($settings_section === 'tracking' && isset($_POST['visitor_counting_mode'])) ? sanitize_key(wp_unslash($_POST['visitor_counting_mode'])) : $existing_settings['visitor_counting_mode'];
if (!in_array($visitor_counting_mode, ['count_all', 'exclude_admins', 'exclude_all_logged_in'], true)) {
$visitor_counting_mode = $existing_settings['visitor_counting_mode'];
}
$settings = [
'visitor_counting_mode' => $visitor_counting_mode,
'exclude_known_bots' => $settings_section === 'tracking' ? (isset($_POST['exclude_known_bots']) ? 1 : 0) : $existing_settings['exclude_known_bots'],
'top_list_rows' => ($settings_section === 'tracking' && isset($_POST['top_list_rows'])) ? max(1, min(100, (int) $_POST['top_list_rows'])) : $existing_settings['top_list_rows'],
'referral_rows' => 0,
'revisit_minutes' => ($settings_section === 'tracking' && isset($_POST['revisit_minutes'])) ? max(1, min(10080, (int) $_POST['revisit_minutes'])) : $existing_settings['revisit_minutes'],
'frontend_label' => ($settings_section === 'tracking' && isset($_POST['frontend_label'])) ? sanitize_text_field(wp_unslash($_POST['frontend_label'])) : $existing_settings['frontend_label'],
'track_post_types' => $submitted_post_types,
'track_front_page' => $settings_section === 'tracking' ? (isset($_POST['track_front_page']) ? 1 : 0) : $existing_settings['track_front_page'],
'track_posts_page' => $settings_section === 'tracking' ? (isset($_POST['track_posts_page']) ? 1 : 0) : $existing_settings['track_posts_page'],
'track_archives' => isset($_POST['track_archives']) ? 1 : (array_key_exists('track_archives', $_POST) ? 0 : $existing_settings['track_archives']),
'track_search' => isset($_POST['track_search']) ? 1 : (array_key_exists('track_search', $_POST) ? 0 : $existing_settings['track_search']),
'track_404' => isset($_POST['track_404']) ? 1 : (array_key_exists('track_404', $_POST) ? 0 : $existing_settings['track_404']),
'ipinfo_token' => ($settings_section === 'tracking' && isset($_POST['ipinfo_token'])) ? sanitize_text_field(wp_unslash($_POST['ipinfo_token'])) : $existing_settings['ipinfo_token'],
'google_client_id' => ($settings_section === 'google' && isset($_POST['google_client_id'])) ? sanitize_text_field(wp_unslash($_POST['google_client_id'])) : $existing_settings['google_client_id'],
'google_client_secret' => ($settings_section === 'google' && isset($_POST['google_client_secret'])) ? sanitize_text_field(wp_unslash($_POST['google_client_secret'])) : $existing_settings['google_client_secret'],
'google_property' => ($settings_section === 'google' && isset($_POST['google_property'])) ? sanitize_text_field(wp_unslash($_POST['google_property'])) : $existing_settings['google_property'],
'google_access_token' => $existing_settings['google_access_token'],
'google_refresh_token' => $existing_settings['google_refresh_token'],
'google_token_expires_at' => $existing_settings['google_token_expires_at'],
'google_connected_email' => $existing_settings['google_connected_email'],
'google_site_verification_token' => $existing_settings['google_site_verification_token'],
'google_site_verification_method' => $existing_settings['google_site_verification_method'],
'google_last_sync_month' => $existing_settings['google_last_sync_month'],
];
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());
$wpdb->query('TRUNCATE TABLE ' . self::landing_table_name());
$wpdb->query('TRUNCATE TABLE ' . self::unique_total_table_name());
$wpdb->query('TRUNCATE TABLE ' . self::unique_page_table_name());
$wpdb->query('TRUNCATE TABLE ' . self::unique_landing_table_name());
$wpdb->query('TRUNCATE TABLE ' . self::unique_referral_table_name());
$wpdb->query('TRUNCATE TABLE ' . self::unique_dimension_table_name());
$wpdb->query('TRUNCATE TABLE ' . self::search_query_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();
}