3797 lines
191 KiB
PHP
3797 lines
191 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: Ansico Stat Plugin
|
|
* Plugin URI: https://ansico.dk/Ansico/Ansico-Stat-plugin
|
|
* Description: Simple WP plugin with basic website statistics.
|
|
* Version: 1.1.0.1
|
|
* Author: Andreas Andersen (Ansico)
|
|
* Author URI: https://ansico.dk
|
|
* Text Domain: ansico-stat-plugin
|
|
* Requires at least: 6.0
|
|
* Requires PHP: 7.4
|
|
* License: GPLv2 or later
|
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
if (!class_exists('Ansico_Stat_Plugin')) {
|
|
class Ansico_Stat_Plugin {
|
|
const VERSION = '1.1.0.1';
|
|
const OPTION_KEY = 'ansico_stat_plugin_settings';
|
|
const INSTALL_OPTION_KEY = 'ansico_stat_plugin_install_date';
|
|
const TOTAL_META_KEY = 'post_views_count';
|
|
const COOKIE_PREFIX = 'ansico_stat_seen_';
|
|
const COOKIE_TTL = 1800; // 30 minutes
|
|
const SCHEMA_VERSION_OPTION_KEY = 'ansico_stat_plugin_schema_version';
|
|
const MENU_SLUG_SETTINGS = 'ansico-stat-plugin-settings';
|
|
const MENU_SLUG_STATS = 'ansico-stat-plugin';
|
|
const MENU_SLUG_YEARLY = 'ansico-stat-plugin-yearly';
|
|
const MENU_SLUG_SINGLE = 'ansico-stat-plugin-single';
|
|
const COOKIE_TTL_MESSAGE = 1800;
|
|
|
|
public function __construct() {
|
|
register_activation_hook(__FILE__, [__CLASS__, 'activate']);
|
|
register_deactivation_hook(__FILE__, [__CLASS__, 'deactivate']);
|
|
|
|
add_action('init', [$this, 'load_textdomain']);
|
|
add_action('init', [$this, 'maybe_upgrade_schema'], 5);
|
|
add_action('template_redirect', [$this, 'maybe_track_view']);
|
|
add_filter('the_content', [$this, 'append_admin_view_count_to_content'], 999);
|
|
add_action('wp_footer', [$this, 'render_admin_view_count_fallback'], 99);
|
|
|
|
add_action('admin_menu', [$this, 'register_admin_page']);
|
|
add_action('admin_bar_menu', [$this, 'register_frontend_admin_bar_link'], 90);
|
|
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
|
|
add_action('wp_dashboard_setup', [$this, 'register_dashboard_widget']);
|
|
add_action('admin_post_ansico_stat_export_csv', [$this, 'handle_export_csv']);
|
|
add_action('admin_post_ansico_stat_export_single_csv', [$this, 'handle_export_single_csv']);
|
|
add_action('admin_post_ansico_stat_reset_all', [$this, 'handle_reset_all_views']);
|
|
add_action('admin_post_ansico_stat_save_settings', [$this, 'handle_save_settings']);
|
|
|
|
add_action('admin_init', [$this, 'register_admin_columns']);
|
|
add_action('restrict_manage_posts', [$this, 'render_admin_view_filter']);
|
|
add_action('pre_get_posts', [$this, 'handle_admin_list_sorting']);
|
|
add_filter('posts_clauses', [$this, 'add_monthly_views_sorting_clauses'], 10, 2);
|
|
}
|
|
|
|
public static function activate() {
|
|
global $wpdb;
|
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
|
|
$charset_collate = $wpdb->get_charset_collate();
|
|
$daily_table = self::daily_table_name();
|
|
$post_daily_table = self::post_daily_table_name();
|
|
$page_daily_table = self::page_daily_table_name();
|
|
$referral_table = self::referral_table_name();
|
|
$dimension_table = self::dimension_table_name();
|
|
|
|
$sql_daily = "CREATE TABLE {$daily_table} (
|
|
stat_date date NOT NULL,
|
|
views bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (stat_date)
|
|
) {$charset_collate};";
|
|
|
|
$sql_post_daily = "CREATE TABLE {$post_daily_table} (
|
|
post_id bigint(20) unsigned NOT NULL,
|
|
stat_date date NOT NULL,
|
|
views bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
unique_visitors bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (post_id, stat_date),
|
|
KEY stat_date (stat_date),
|
|
KEY views (views)
|
|
) {$charset_collate};";
|
|
|
|
$sql_page_daily = "CREATE TABLE {$page_daily_table} (
|
|
page_key varchar(191) NOT NULL,
|
|
page_type varchar(32) NOT NULL,
|
|
object_id bigint(20) unsigned NULL DEFAULT NULL,
|
|
page_label varchar(255) NOT NULL,
|
|
page_url text NULL,
|
|
stat_date date NOT NULL,
|
|
views bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
unique_visitors bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (page_key, stat_date),
|
|
KEY stat_date (stat_date),
|
|
KEY page_type (page_type),
|
|
KEY object_id (object_id),
|
|
KEY views (views)
|
|
) {$charset_collate};";
|
|
|
|
$sql_referral = "CREATE TABLE {$referral_table} (
|
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
stat_date date NOT NULL,
|
|
category varchar(32) NOT NULL,
|
|
source_url text NULL,
|
|
source_host varchar(191) NULL,
|
|
visits bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY unique_referral (stat_date, category, source_host(191), source_url(191)),
|
|
KEY stat_date (stat_date),
|
|
KEY category (category),
|
|
KEY source_host (source_host)
|
|
) {$charset_collate};";
|
|
|
|
$sql_dimensions = "CREATE TABLE {$dimension_table} (
|
|
stat_date date NOT NULL,
|
|
dimension_type varchar(32) NOT NULL,
|
|
dimension_value varchar(191) NOT NULL,
|
|
views bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (stat_date, dimension_type, dimension_value),
|
|
KEY dimension_type (dimension_type),
|
|
KEY views (views)
|
|
) {$charset_collate};";
|
|
|
|
dbDelta($sql_daily);
|
|
dbDelta($sql_post_daily);
|
|
dbDelta($sql_page_daily);
|
|
dbDelta($sql_referral);
|
|
dbDelta($sql_dimensions);
|
|
|
|
if (!get_option(self::INSTALL_OPTION_KEY)) {
|
|
update_option(self::INSTALL_OPTION_KEY, current_time('Y-m-d'), false);
|
|
}
|
|
update_option(self::SCHEMA_VERSION_OPTION_KEY, self::VERSION, false);
|
|
}
|
|
|
|
public static function deactivate() {
|
|
// Keep data on deactivation.
|
|
}
|
|
|
|
public function maybe_upgrade_schema() {
|
|
$stored_version = (string) get_option(self::SCHEMA_VERSION_OPTION_KEY, '');
|
|
if ($stored_version === self::VERSION) {
|
|
return;
|
|
}
|
|
|
|
self::activate();
|
|
}
|
|
|
|
|
|
public function load_textdomain() {
|
|
load_plugin_textdomain('ansico-stat-plugin', false, dirname(plugin_basename(__FILE__)) . '/languages');
|
|
}
|
|
|
|
public static function daily_table_name() {
|
|
global $wpdb;
|
|
return $wpdb->prefix . 'ansico_stat_daily';
|
|
}
|
|
|
|
public static function post_daily_table_name() {
|
|
global $wpdb;
|
|
return $wpdb->prefix . 'ansico_stat_post_daily';
|
|
}
|
|
|
|
public static function page_daily_table_name() {
|
|
global $wpdb;
|
|
return $wpdb->prefix . 'ansico_stat_page_daily';
|
|
}
|
|
|
|
public static function referral_table_name() {
|
|
global $wpdb;
|
|
return $wpdb->prefix . 'ansico_stat_referrals';
|
|
}
|
|
|
|
public static function dimension_table_name() {
|
|
global $wpdb;
|
|
return $wpdb->prefix . 'ansico_stat_dimensions';
|
|
}
|
|
|
|
protected function get_default_track_post_types() {
|
|
$post_types = get_post_types(['public' => true], 'names');
|
|
$post_types = array_filter($post_types, function($post_type) {
|
|
return !in_array($post_type, ['attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block', 'wp_template', 'wp_template_part', 'wp_navigation', 'wp_global_styles'], true);
|
|
});
|
|
return array_values($post_types);
|
|
}
|
|
|
|
protected function get_settings() {
|
|
$settings = get_option(self::OPTION_KEY, []);
|
|
if (!is_array($settings)) {
|
|
$settings = [];
|
|
}
|
|
|
|
$settings = wp_parse_args($settings, [
|
|
'visitor_counting_mode' => 'count_all',
|
|
'exclude_known_bots' => 1,
|
|
'top_list_rows' => 10,
|
|
'referral_rows' => 25,
|
|
'revisit_minutes' => 30,
|
|
'frontend_label' => 'Views',
|
|
'track_post_types' => $this->get_default_track_post_types(),
|
|
'track_front_page' => 1,
|
|
'track_posts_page' => 1,
|
|
'track_archives' => 1,
|
|
'track_search' => 1,
|
|
'track_404' => 1,
|
|
]);
|
|
|
|
if (!is_array($settings['track_post_types'])) {
|
|
$settings['track_post_types'] = $this->get_default_track_post_types();
|
|
}
|
|
|
|
$settings['track_post_types'] = array_values(array_filter(array_map('sanitize_key', $settings['track_post_types'])));
|
|
$settings['visitor_counting_mode'] = in_array($settings['visitor_counting_mode'], ['count_all', 'exclude_admins', 'exclude_all_logged_in'], true) ? $settings['visitor_counting_mode'] : 'count_all';
|
|
$settings['revisit_minutes'] = max(1, min(10080, (int) $settings['revisit_minutes']));
|
|
$settings['frontend_label'] = sanitize_text_field((string) $settings['frontend_label']);
|
|
if ($settings['frontend_label'] === '') {
|
|
$settings['frontend_label'] = 'Views';
|
|
}
|
|
|
|
return $settings;
|
|
}
|
|
|
|
protected function get_cookie_ttl() {
|
|
$settings = $this->get_settings();
|
|
return max(60, (int) $settings['revisit_minutes'] * 60);
|
|
}
|
|
|
|
protected function should_exclude_current_user() {
|
|
$settings = $this->get_settings();
|
|
$mode = $settings['visitor_counting_mode'] ?? 'count_all';
|
|
|
|
if ($mode === 'exclude_all_logged_in' && is_user_logged_in()) {
|
|
return true;
|
|
}
|
|
|
|
if ($mode === 'exclude_admins' && current_user_can('manage_options')) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected function is_tracking_enabled_for_context(array $page_context) {
|
|
$settings = $this->get_settings();
|
|
$page_type = (string) ($page_context['page_type'] ?? '');
|
|
|
|
if (($page_context['kind'] ?? '') === 'singular') {
|
|
$post_type = sanitize_key((string) ($page_context['post_type'] ?? get_post_type((int) ($page_context['post_id'] ?? 0))));
|
|
return in_array($post_type, $settings['track_post_types'], true);
|
|
}
|
|
|
|
if ($page_type === 'front_page') {
|
|
return !empty($settings['track_front_page']);
|
|
}
|
|
|
|
if ($page_type === 'home') {
|
|
return !empty($settings['track_posts_page']);
|
|
}
|
|
|
|
if ($page_type === 'search') {
|
|
return !empty($settings['track_search']);
|
|
}
|
|
|
|
if ($page_type === '404') {
|
|
return !empty($settings['track_404']);
|
|
}
|
|
|
|
return !empty($settings['track_archives']);
|
|
}
|
|
|
|
protected function get_frontend_icon_markup() {
|
|
return '<span class="ansico-stat-frontend-icon" aria-hidden="true" style="display:inline-flex;vertical-align:middle;margin-right:.45rem;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="3" y="11" width="3" height="9" rx="1" fill="currentColor"/><rect x="10.5" y="7" width="3" height="13" rx="1" fill="currentColor"/><rect x="18" y="3" width="3" height="17" rx="1" fill="currentColor"/></svg></span>';
|
|
}
|
|
|
|
protected function get_country_code() {
|
|
$candidates = [
|
|
$_SERVER['HTTP_CF_IPCOUNTRY'] ?? '',
|
|
$_SERVER['GEOIP_COUNTRY_CODE'] ?? '',
|
|
$_SERVER['HTTP_X_COUNTRY_CODE'] ?? '',
|
|
$_SERVER['HTTP_X_APPENGINE_COUNTRY'] ?? '',
|
|
];
|
|
|
|
foreach ($candidates as $candidate) {
|
|
$code = strtoupper(sanitize_text_field((string) $candidate));
|
|
if (preg_match('/^[A-Z]{2}$/', $code)) {
|
|
return $code;
|
|
}
|
|
}
|
|
|
|
return 'Unknown';
|
|
}
|
|
|
|
protected function get_country_label(string $country_code) {
|
|
$country_code = strtoupper(trim($country_code));
|
|
if ($country_code === '' || $country_code === 'UNKNOWN') {
|
|
return __('Unknown', 'ansico-stat-plugin');
|
|
}
|
|
|
|
if (function_exists('locale_get_display_region')) {
|
|
$label = locale_get_display_region('-' . $country_code, get_locale());
|
|
if (is_string($label) && $label !== '') {
|
|
return $label;
|
|
}
|
|
}
|
|
|
|
return $country_code;
|
|
}
|
|
|
|
protected function get_device_type() {
|
|
$user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
|
|
|
|
if ($user_agent === '') {
|
|
return 'Unknown';
|
|
}
|
|
|
|
if (strpos($user_agent, 'tablet') !== false || strpos($user_agent, 'ipad') !== false || strpos($user_agent, 'kindle') !== false || strpos($user_agent, 'playbook') !== false) {
|
|
return 'Tablet';
|
|
}
|
|
|
|
if (strpos($user_agent, 'mobile') !== false || strpos($user_agent, 'iphone') !== false || strpos($user_agent, 'android') !== false || strpos($user_agent, 'phone') !== false) {
|
|
return 'Mobile';
|
|
}
|
|
|
|
return 'Desktop';
|
|
}
|
|
|
|
protected function get_browser_name() {
|
|
$user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
|
|
|
|
$browsers = [
|
|
'edg/' => 'Edge',
|
|
'opr/' => 'Opera',
|
|
'opera' => 'Opera',
|
|
'brave' => 'Brave',
|
|
'firefox' => 'Firefox',
|
|
'samsungbrowser' => 'Samsung Internet',
|
|
'chrome' => 'Chrome',
|
|
'crios' => 'Chrome',
|
|
'safari' => 'Safari',
|
|
'trident/' => 'Internet Explorer',
|
|
'msie' => 'Internet Explorer',
|
|
];
|
|
|
|
foreach ($browsers as $needle => $label) {
|
|
if (strpos($user_agent, $needle) !== false) {
|
|
if ($label === 'Safari' && (strpos($user_agent, 'chrome') !== false || strpos($user_agent, 'crios') !== false || strpos($user_agent, 'android') !== false)) {
|
|
continue;
|
|
}
|
|
return $label;
|
|
}
|
|
}
|
|
|
|
return 'Other';
|
|
}
|
|
|
|
protected function get_os_name() {
|
|
$user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
|
|
|
|
$systems = [
|
|
'windows nt' => 'Windows',
|
|
'iphone' => 'iOS',
|
|
'ipad' => 'iPadOS',
|
|
'mac os x' => 'macOS',
|
|
'android' => 'Android',
|
|
'linux' => 'Linux',
|
|
'cros' => 'Chrome OS',
|
|
];
|
|
|
|
foreach ($systems as $needle => $label) {
|
|
if (strpos($user_agent, $needle) !== false) {
|
|
return $label;
|
|
}
|
|
}
|
|
|
|
return 'Other';
|
|
}
|
|
|
|
protected function increment_dimension_views(string $today) {
|
|
global $wpdb;
|
|
$table = self::dimension_table_name();
|
|
|
|
$dimensions = [
|
|
'country' => $this->get_country_code(),
|
|
'device' => $this->get_device_type(),
|
|
'browser' => $this->get_browser_name(),
|
|
'os' => $this->get_os_name(),
|
|
];
|
|
|
|
foreach ($dimensions as $dimension_type => $dimension_value) {
|
|
$dimension_value = substr(sanitize_text_field((string) $dimension_value), 0, 191);
|
|
if ($dimension_value === '') {
|
|
$dimension_value = 'Unknown';
|
|
}
|
|
|
|
$wpdb->query($wpdb->prepare(
|
|
"INSERT INTO {$table} (stat_date, dimension_type, dimension_value, views)
|
|
VALUES (%s, %s, %s, 1)
|
|
ON DUPLICATE KEY UPDATE views = views + 1",
|
|
$today,
|
|
$dimension_type,
|
|
$dimension_value
|
|
));
|
|
}
|
|
}
|
|
|
|
|
|
protected function is_known_bot_request() {
|
|
$user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? strtolower((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
|
|
if ($user_agent === '') {
|
|
return true;
|
|
}
|
|
|
|
$bot_markers = [
|
|
'bot', 'crawl', 'crawler', 'spider', 'slurp', 'facebookexternalhit', 'bingpreview',
|
|
'headless', 'preview', 'python-requests', 'curl', 'wget', 'uptimerobot', 'monitoring',
|
|
'phantomjs', 'feedfetcher', 'google-read-aloud', 'scanner', 'validator', 'scrapy',
|
|
'axios', 'go-http-client', 'node-fetch', 'httpclient', 'libwww-perl'
|
|
];
|
|
|
|
foreach ($bot_markers as $marker) {
|
|
if (strpos($user_agent, $marker) !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!empty($_SERVER['HTTP_X_PURPOSE']) || !empty($_SERVER['HTTP_X_MOZ']) || (!empty($_SERVER['HTTP_SEC_FETCH_SITE']) && strtolower((string) $_SERVER['HTTP_SEC_FETCH_SITE']) === 'none' && strpos($user_agent, 'mozilla') === false)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected function is_trackable_post($post) {
|
|
if (!$post || empty($post->ID) || !is_object($post)) {
|
|
return false;
|
|
}
|
|
|
|
$post_type_obj = get_post_type_object($post->post_type);
|
|
if (!$post_type_obj || empty($post_type_obj->public)) {
|
|
return false;
|
|
}
|
|
|
|
if (post_password_required($post)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function get_referer_data() {
|
|
$referer = isset($_SERVER['HTTP_REFERER']) ? trim((string) wp_unslash($_SERVER['HTTP_REFERER'])) : '';
|
|
$site_host = wp_parse_url(home_url(), PHP_URL_HOST);
|
|
$site_host = $site_host ? strtolower((string) $site_host) : '';
|
|
|
|
if ($referer === '') {
|
|
return [
|
|
'category' => 'direct',
|
|
'source_url' => '',
|
|
'source_host'=> '',
|
|
];
|
|
}
|
|
|
|
$host = wp_parse_url($referer, PHP_URL_HOST);
|
|
$host = $host ? strtolower((string) $host) : '';
|
|
|
|
if ($host === '' || $host === $site_host || preg_replace('/^www\./', '', $host) === preg_replace('/^www\./', '', $site_host)) {
|
|
return [
|
|
'category' => 'direct',
|
|
'source_url' => '',
|
|
'source_host'=> '',
|
|
];
|
|
}
|
|
|
|
$search_hosts = [
|
|
'google.', 'bing.com', 'search.yahoo.', 'duckduckgo.com', 'yandex.', 'baidu.com', 'ecosia.org',
|
|
'startpage.com', 'qwant.com', 'search.brave.com'
|
|
];
|
|
foreach ($search_hosts as $needle) {
|
|
if (strpos($host, $needle) !== false) {
|
|
return [
|
|
'category' => 'search',
|
|
'source_url' => esc_url_raw($referer),
|
|
'source_host'=> sanitize_text_field($host),
|
|
];
|
|
}
|
|
}
|
|
|
|
$social_hosts = [
|
|
'facebook.com', 'm.facebook.com', 'l.facebook.com', 'lm.facebook.com', 'instagram.com',
|
|
't.co', 'twitter.com', 'x.com', 'linkedin.com', 'lnkd.in', 'pinterest.', 'reddit.com',
|
|
'youtube.com', 'youtu.be', 'tiktok.com', 'snapchat.com', 'threads.net', 'messenger.com'
|
|
];
|
|
foreach ($social_hosts as $needle) {
|
|
if (strpos($host, $needle) !== false) {
|
|
return [
|
|
'category' => 'social',
|
|
'source_url' => esc_url_raw($referer),
|
|
'source_host'=> sanitize_text_field($host),
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'category' => 'website',
|
|
'source_url' => esc_url_raw($referer),
|
|
'source_host'=> sanitize_text_field($host),
|
|
];
|
|
}
|
|
|
|
public function maybe_track_view() {
|
|
if (is_admin() || wp_doing_ajax() || wp_doing_cron() || is_feed() || is_preview() || is_trackback() || is_robots()) {
|
|
return;
|
|
}
|
|
|
|
if (!$this->is_trackable_request()) {
|
|
return;
|
|
}
|
|
|
|
$settings = $this->get_settings();
|
|
if ($this->should_exclude_current_user()) {
|
|
return;
|
|
}
|
|
|
|
if (!empty($settings['exclude_known_bots']) && $this->is_known_bot_request()) {
|
|
return;
|
|
}
|
|
|
|
$page_context = $this->get_current_page_context();
|
|
if (!$page_context || !$this->is_tracking_enabled_for_context($page_context)) {
|
|
return;
|
|
}
|
|
|
|
$view_cookie_name = self::COOKIE_PREFIX . md5($page_context['cookie_key']);
|
|
$unique_cookie_name = self::COOKIE_PREFIX . 'unique_' . md5($page_context['cookie_key'] . '|' . current_time('Y-m-d'));
|
|
$is_unique_visitor = !isset($_COOKIE[$unique_cookie_name]);
|
|
|
|
if (isset($_COOKIE[$view_cookie_name])) {
|
|
return;
|
|
}
|
|
|
|
$referer_data = $this->get_referer_data();
|
|
if ($page_context['kind'] === 'singular') {
|
|
$this->increment_post_views((int) $page_context['post_id'], $referer_data, $is_unique_visitor);
|
|
} else {
|
|
$this->increment_non_singular_views($page_context, $referer_data, $is_unique_visitor);
|
|
}
|
|
|
|
if (!headers_sent()) {
|
|
setcookie($view_cookie_name, '1', time() + $this->get_cookie_ttl(), COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true);
|
|
$_COOKIE[$view_cookie_name] = '1';
|
|
|
|
if ($is_unique_visitor) {
|
|
$expires = strtotime('tomorrow', current_time('timestamp'));
|
|
if ($expires <= time()) {
|
|
$expires = time() + DAY_IN_SECONDS;
|
|
}
|
|
setcookie($unique_cookie_name, '1', $expires, COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true);
|
|
$_COOKIE[$unique_cookie_name] = '1';
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function is_trackable_request() {
|
|
return is_singular() || is_404() || is_home() || is_front_page() || is_post_type_archive() || is_category() || is_tag() || is_tax() || is_author() || is_date() || is_search() || is_archive();
|
|
}
|
|
|
|
protected function normalize_request_uri() {
|
|
$uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '/';
|
|
$path = wp_parse_url($uri, PHP_URL_PATH);
|
|
$path = is_string($path) && $path !== '' ? $path : '/';
|
|
return untrailingslashit($path) ?: '/';
|
|
}
|
|
|
|
protected function get_current_page_context() {
|
|
if (is_admin()) {
|
|
return null;
|
|
}
|
|
|
|
if (is_singular()) {
|
|
$post = $this->get_current_singular_post();
|
|
if (!$post) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'kind' => 'singular',
|
|
'post_id' => (int) $post->ID,
|
|
'post_type' => sanitize_key((string) $post->post_type),
|
|
'cookie_key' => 'post:' . (int) $post->ID,
|
|
'page_key' => 'post:' . (int) $post->ID,
|
|
'page_type' => sanitize_key((string) $post->post_type),
|
|
'object_id' => (int) $post->ID,
|
|
'label' => get_the_title($post->ID) ?: __('(no title)', 'ansico-stat-plugin'),
|
|
'url' => get_permalink($post->ID) ?: '',
|
|
];
|
|
}
|
|
|
|
$request_path = $this->normalize_request_uri();
|
|
$base = [
|
|
'kind' => 'archive',
|
|
'cookie_key' => $request_path,
|
|
'page_key' => '',
|
|
'page_type' => 'archive',
|
|
'object_id' => 0,
|
|
'label' => '',
|
|
'url' => home_url($request_path . '/'),
|
|
];
|
|
|
|
if (is_404()) {
|
|
$base['page_type'] = '404';
|
|
$base['page_key'] = '404:' . md5($request_path);
|
|
$base['label'] = sprintf(__('404: %s', 'ansico-stat-plugin'), $request_path);
|
|
$base['url'] = home_url($request_path === '/' ? '/' : $request_path . '/');
|
|
return $base;
|
|
}
|
|
|
|
if (is_category() || is_tag() || is_tax()) {
|
|
$term = get_queried_object();
|
|
if (!$term || empty($term->term_id)) {
|
|
return null;
|
|
}
|
|
$taxonomy = is_string($term->taxonomy ?? '') ? $term->taxonomy : 'taxonomy';
|
|
$base['page_type'] = $taxonomy;
|
|
$base['page_key'] = 'term:' . $taxonomy . ':' . (int) $term->term_id;
|
|
$base['cookie_key'] = $base['page_key'];
|
|
$base['object_id'] = (int) $term->term_id;
|
|
$base['label'] = single_term_title('', false) ?: ($term->name ?? $request_path);
|
|
$base['url'] = get_term_link($term);
|
|
return $base;
|
|
}
|
|
|
|
if (is_author()) {
|
|
$author = get_queried_object();
|
|
if (!$author || empty($author->ID)) {
|
|
return null;
|
|
}
|
|
$base['page_type'] = 'author';
|
|
$base['page_key'] = 'author:' . (int) $author->ID;
|
|
$base['cookie_key'] = $base['page_key'];
|
|
$base['object_id'] = (int) $author->ID;
|
|
$base['label'] = get_the_author_meta('display_name', (int) $author->ID) ?: $request_path;
|
|
$base['url'] = get_author_posts_url((int) $author->ID);
|
|
return $base;
|
|
}
|
|
|
|
if (is_post_type_archive()) {
|
|
$post_type = get_query_var('post_type');
|
|
$post_type = is_array($post_type) ? reset($post_type) : $post_type;
|
|
$post_type = sanitize_key((string) $post_type);
|
|
if ($post_type === '') {
|
|
$post_type = 'archive';
|
|
}
|
|
$base['page_type'] = 'post_type_archive';
|
|
$base['page_key'] = 'post_type_archive:' . $post_type;
|
|
$base['cookie_key'] = $base['page_key'];
|
|
$base['label'] = post_type_archive_title('', false) ?: $request_path;
|
|
$archive_link = get_post_type_archive_link($post_type);
|
|
$base['url'] = $archive_link ?: $base['url'];
|
|
return $base;
|
|
}
|
|
|
|
if (is_home() || is_front_page()) {
|
|
$base['page_type'] = is_front_page() ? 'front_page' : 'home';
|
|
$base['page_key'] = $base['page_type'];
|
|
$base['cookie_key'] = $base['page_key'];
|
|
$base['label'] = is_front_page() ? __('Front page', 'ansico-stat-plugin') : __('Posts page', 'ansico-stat-plugin');
|
|
$base['url'] = home_url('/');
|
|
return $base;
|
|
}
|
|
|
|
if (is_date()) {
|
|
$base['page_type'] = 'date';
|
|
$base['page_key'] = 'date:' . md5($request_path);
|
|
$base['label'] = wp_get_document_title() ?: $request_path;
|
|
return $base;
|
|
}
|
|
|
|
if (is_search()) {
|
|
$search_query = get_search_query();
|
|
$base['page_type'] = 'search';
|
|
$base['page_key'] = 'search:' . md5((string) $search_query);
|
|
$base['cookie_key'] = 'search:' . md5((string) $search_query);
|
|
$base['label'] = sprintf(__('Search: %s', 'ansico-stat-plugin'), $search_query ?: __('(empty)', 'ansico-stat-plugin'));
|
|
$base['url'] = get_search_link($search_query);
|
|
return $base;
|
|
}
|
|
|
|
if (is_archive()) {
|
|
$base['page_type'] = 'archive';
|
|
$base['page_key'] = 'archive:' . md5($request_path);
|
|
$base['label'] = wp_get_document_title() ?: $request_path;
|
|
return $base;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function increment_post_views(int $post_id, array $referer_data = [], bool $is_unique_visitor = false) {
|
|
global $wpdb;
|
|
|
|
$today = current_time('Y-m-d');
|
|
$daily_table = self::daily_table_name();
|
|
$post_daily_table = self::post_daily_table_name();
|
|
$page_daily_table = self::page_daily_table_name();
|
|
$referral_table = self::referral_table_name();
|
|
|
|
$current_total = (int) get_post_meta($post_id, self::TOTAL_META_KEY, true);
|
|
update_post_meta($post_id, self::TOTAL_META_KEY, $current_total + 1);
|
|
|
|
$wpdb->query($wpdb->prepare(
|
|
"INSERT INTO {$daily_table} (stat_date, views)
|
|
VALUES (%s, 1)
|
|
ON DUPLICATE KEY UPDATE views = views + 1",
|
|
$today
|
|
));
|
|
|
|
$wpdb->query($wpdb->prepare(
|
|
"INSERT INTO {$post_daily_table} (post_id, stat_date, views, unique_visitors)
|
|
VALUES (%d, %s, 1, %d)
|
|
ON DUPLICATE KEY UPDATE views = views + 1, unique_visitors = unique_visitors + VALUES(unique_visitors)",
|
|
$post_id,
|
|
$today,
|
|
$is_unique_visitor ? 1 : 0
|
|
));
|
|
|
|
$category = isset($referer_data['category']) ? sanitize_key($referer_data['category']) : 'direct';
|
|
$source_url = isset($referer_data['source_url']) ? esc_url_raw((string) $referer_data['source_url']) : '';
|
|
$source_host = isset($referer_data['source_host']) ? sanitize_text_field((string) $referer_data['source_host']) : '';
|
|
|
|
$wpdb->query($wpdb->prepare(
|
|
"INSERT INTO {$referral_table} (stat_date, category, source_url, source_host, visits)
|
|
VALUES (%s, %s, %s, %s, 1)
|
|
ON DUPLICATE KEY UPDATE visits = visits + 1",
|
|
$today,
|
|
$category,
|
|
$source_url,
|
|
$source_host
|
|
));
|
|
|
|
$this->increment_dimension_views($today);
|
|
}
|
|
|
|
protected function increment_non_singular_views(array $page_context, array $referer_data = [], bool $is_unique_visitor = false) {
|
|
global $wpdb;
|
|
|
|
$today = current_time('Y-m-d');
|
|
$daily_table = self::daily_table_name();
|
|
$page_daily_table = self::page_daily_table_name();
|
|
$referral_table = self::referral_table_name();
|
|
|
|
$wpdb->query($wpdb->prepare(
|
|
"INSERT INTO {$daily_table} (stat_date, views)
|
|
VALUES (%s, 1)
|
|
ON DUPLICATE KEY UPDATE views = views + 1",
|
|
$today
|
|
));
|
|
|
|
$page_key = substr(sanitize_text_field((string) $page_context['page_key']), 0, 191);
|
|
$page_type = substr(sanitize_key((string) $page_context['page_type']), 0, 32);
|
|
$object_id = !empty($page_context['object_id']) ? (int) $page_context['object_id'] : null;
|
|
$label = sanitize_text_field((string) $page_context['label']);
|
|
$url = isset($page_context['url']) ? esc_url_raw((string) $page_context['url']) : '';
|
|
|
|
$wpdb->query($wpdb->prepare(
|
|
"INSERT INTO {$page_daily_table} (page_key, page_type, object_id, page_label, page_url, stat_date, views, unique_visitors)
|
|
VALUES (%s, %s, %s, %s, %s, %s, 1, %d)
|
|
ON DUPLICATE KEY UPDATE views = views + 1, unique_visitors = unique_visitors + VALUES(unique_visitors), page_label = VALUES(page_label), page_url = VALUES(page_url)",
|
|
$page_key,
|
|
$page_type,
|
|
$object_id,
|
|
$label,
|
|
$url,
|
|
$today,
|
|
$is_unique_visitor ? 1 : 0
|
|
));
|
|
|
|
$category = isset($referer_data['category']) ? sanitize_key($referer_data['category']) : 'direct';
|
|
$source_url = isset($referer_data['source_url']) ? esc_url_raw((string) $referer_data['source_url']) : '';
|
|
$source_host = isset($referer_data['source_host']) ? sanitize_text_field((string) $referer_data['source_host']) : '';
|
|
|
|
$wpdb->query($wpdb->prepare(
|
|
"INSERT INTO {$referral_table} (stat_date, category, source_url, source_host, visits)
|
|
VALUES (%s, %s, %s, %s, 1)
|
|
ON DUPLICATE KEY UPDATE visits = visits + 1",
|
|
$today,
|
|
$category,
|
|
$source_url,
|
|
$source_host
|
|
));
|
|
|
|
$this->increment_dimension_views($today);
|
|
}
|
|
|
|
protected function get_non_singular_lifetime_views(string $page_key) {
|
|
global $wpdb;
|
|
$table = self::page_daily_table_name();
|
|
$views = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT SUM(views) FROM {$table} WHERE page_key = %s",
|
|
$page_key
|
|
));
|
|
return (int) $views;
|
|
}
|
|
|
|
protected function get_current_singular_post() {
|
|
if (is_admin() || !is_singular()) {
|
|
return null;
|
|
}
|
|
|
|
$post = get_queried_object();
|
|
if (!$this->is_trackable_post($post)) {
|
|
return null;
|
|
}
|
|
|
|
return $post;
|
|
}
|
|
|
|
protected function get_admin_view_markup(array $page_context) {
|
|
if (!$this->is_tracking_enabled_for_context($page_context)) {
|
|
return '';
|
|
}
|
|
|
|
$views = 0;
|
|
if (($page_context['kind'] ?? '') === 'singular' && !empty($page_context['post_id'])) {
|
|
$views = (int) get_post_meta((int) $page_context['post_id'], self::TOTAL_META_KEY, true);
|
|
} elseif (!empty($page_context['page_key'])) {
|
|
$views = $this->get_non_singular_lifetime_views((string) $page_context['page_key']);
|
|
}
|
|
|
|
$settings = $this->get_settings();
|
|
$label = sanitize_text_field((string) ($settings['frontend_label'] ?? 'Views'));
|
|
if ($label === '') {
|
|
$label = 'Views';
|
|
}
|
|
|
|
$markup = '<div class="ansico-stat-frontend-views" data-ansico-stat-views="1" style="margin:2.75rem 0 1.75rem; padding:1rem 1rem 0; border-top:1px solid #ddd; font-size:14px; opacity:.95; display:flex; align-items:center; gap:.25rem; flex-wrap:wrap;">';
|
|
$markup .= $this->get_frontend_icon_markup();
|
|
$markup .= '<strong>' . esc_html($label . ':') . '</strong> ' . esc_html(number_format_i18n($views));
|
|
$markup .= '</div>';
|
|
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);
|
|
?>
|
|
<script>
|
|
(function() {
|
|
var markup = <?php echo $markup; ?>;
|
|
if (!markup || document.querySelector('[data-ansico-stat-views="1"]')) {
|
|
return;
|
|
}
|
|
var selectors = [
|
|
'.entry-content', '.post-content', '.content', 'main', 'article',
|
|
'.site-main', '.archive-description', '.page-content', '.taxonomy-description'
|
|
];
|
|
var target = null;
|
|
for (var i = 0; i < selectors.length; i++) {
|
|
target = document.querySelector(selectors[i]);
|
|
if (target) {
|
|
break;
|
|
}
|
|
}
|
|
if (!target) {
|
|
target = document.querySelector('#secondary, .widget-area, aside, footer, .site-footer');
|
|
}
|
|
if (!target) {
|
|
target = document.body;
|
|
}
|
|
var wrapper = document.createElement('div');
|
|
wrapper.innerHTML = markup;
|
|
target.appendChild(wrapper.firstElementChild);
|
|
})();
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
public function register_admin_page() {
|
|
add_menu_page(
|
|
__('Ansico Stat Plugin', 'ansico-stat-plugin'),
|
|
__('Ansico Stat Plugin', 'ansico-stat-plugin'),
|
|
'manage_options',
|
|
self::MENU_SLUG_STATS,
|
|
[$this, 'render_monthly_stats_page'],
|
|
'dashicons-chart-area',
|
|
58
|
|
);
|
|
|
|
add_submenu_page(
|
|
self::MENU_SLUG_STATS,
|
|
__('Monthly statistics', 'ansico-stat-plugin'),
|
|
__('Monthly statistics', 'ansico-stat-plugin'),
|
|
'manage_options',
|
|
self::MENU_SLUG_STATS,
|
|
[$this, 'render_monthly_stats_page']
|
|
);
|
|
|
|
add_submenu_page(
|
|
self::MENU_SLUG_STATS,
|
|
__('Yearly statistics', 'ansico-stat-plugin'),
|
|
__('Yearly statistics', 'ansico-stat-plugin'),
|
|
'manage_options',
|
|
self::MENU_SLUG_YEARLY,
|
|
[$this, 'render_yearly_stats_page']
|
|
);
|
|
|
|
add_submenu_page(
|
|
self::MENU_SLUG_STATS,
|
|
__('Single page statistics', 'ansico-stat-plugin'),
|
|
__('Single page statistics', 'ansico-stat-plugin'),
|
|
'manage_options',
|
|
self::MENU_SLUG_SINGLE,
|
|
[$this, 'render_single_page_stats_page']
|
|
);
|
|
|
|
add_submenu_page(
|
|
self::MENU_SLUG_STATS,
|
|
__('Settings', 'ansico-stat-plugin'),
|
|
__('Settings', 'ansico-stat-plugin'),
|
|
'manage_options',
|
|
self::MENU_SLUG_SETTINGS,
|
|
[$this, 'render_settings_page']
|
|
);
|
|
}
|
|
|
|
public function enqueue_admin_assets($hook) {
|
|
if (!in_array($hook, [
|
|
'index.php',
|
|
'toplevel_page_' . self::MENU_SLUG_STATS,
|
|
'ansico-stat-plugin_page_' . self::MENU_SLUG_YEARLY,
|
|
'ansico-stat-plugin_page_' . self::MENU_SLUG_SINGLE,
|
|
'ansico-stat-plugin_page_' . self::MENU_SLUG_SETTINGS,
|
|
], true)) {
|
|
return;
|
|
}
|
|
|
|
wp_enqueue_style(
|
|
'ansico-stat-admin',
|
|
plugin_dir_url(__FILE__) . 'assets/css/ansico-stat-admin.css',
|
|
[],
|
|
self::VERSION
|
|
);
|
|
}
|
|
|
|
public function register_dashboard_widget() {
|
|
wp_add_dashboard_widget(
|
|
'ansico_stat_dashboard_widget',
|
|
__('Ansico Stat Plugin: Daily Total Views', 'ansico-stat-plugin'),
|
|
[$this, 'render_dashboard_widget']
|
|
);
|
|
}
|
|
|
|
protected function format_post_link(int $post_id, string $label) {
|
|
$permalink = get_permalink($post_id);
|
|
$label = $label ?: __('(no title)', 'ansico-stat-plugin');
|
|
|
|
if (!$permalink) {
|
|
return esc_html($label);
|
|
}
|
|
|
|
return sprintf(
|
|
'<a href="%1$s" title="%2$s" data-permalink="%2$s" target="_blank" rel="noopener noreferrer">%3$s</a>',
|
|
esc_url($permalink),
|
|
esc_attr($permalink),
|
|
esc_html($label)
|
|
);
|
|
}
|
|
|
|
protected function format_external_link(string $url) {
|
|
if ($url === '') {
|
|
return '—';
|
|
}
|
|
|
|
return sprintf(
|
|
'<a href="%1$s" title="%2$s" data-permalink="%2$s" target="_blank" rel="noopener noreferrer">%3$s</a>',
|
|
esc_url($url),
|
|
esc_attr($url),
|
|
esc_html($url)
|
|
);
|
|
}
|
|
|
|
public function render_dashboard_widget() {
|
|
$chart_data = $this->get_daily_chart_data(30);
|
|
$monthly_total = array_sum(wp_list_pluck($chart_data, 'views'));
|
|
$top_posts = $this->get_top_posts_in_period($this->date_days_ago(29), current_time('Y-m-d'), 10);
|
|
|
|
echo '<div class="ansico-stat-widget">';
|
|
echo '<p class="ansico-stat-widget-total">' . esc_html(sprintf(__('Total tracked views in the last 30 days: %s', 'ansico-stat-plugin'), number_format_i18n($monthly_total))) . '</p>';
|
|
echo '<div class="ansico-stat-widget-chart-wrap">';
|
|
echo $this->render_chart_markup($chart_data, 'ansico-stat-widget-chart', ['is_widget' => true]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
echo '</div>';
|
|
|
|
echo '<h4>' . esc_html__('Top 10 views in the last 30 days', 'ansico-stat-plugin') . '</h4>';
|
|
if (empty($top_posts)) {
|
|
echo '<p>' . esc_html__('No tracked views yet.', 'ansico-stat-plugin') . '</p>';
|
|
} else {
|
|
echo '<ol class="ansico-stat-top-list">';
|
|
foreach ($top_posts as $row) {
|
|
$post_id = (int) $row['post_id'];
|
|
$title = get_the_title($post_id) ?: __('(no title)', 'ansico-stat-plugin');
|
|
echo '<li>';
|
|
echo $this->format_post_link($post_id, $title); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
echo ' <span>(' . esc_html(number_format_i18n((int) $row['period_views'])) . ')</span>';
|
|
echo '</li>';
|
|
}
|
|
echo '</ol>';
|
|
}
|
|
|
|
echo '<p><a class="button button-secondary" href="' . esc_url(admin_url('admin.php?page=' . self::MENU_SLUG_STATS)) . '">' . esc_html__('Open full statistics', 'ansico-stat-plugin') . '</a></p>';
|
|
echo '</div>';
|
|
}
|
|
|
|
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 '<p>' . esc_html__('No tracked data for this period.', 'ansico-stat-plugin') . '</p>';
|
|
}
|
|
|
|
$max = max($values ?: [0]);
|
|
$sum = array_sum($values);
|
|
if ($sum <= 0) {
|
|
return '<p>' . esc_html__('No tracked data for this period.', 'ansico-stat-plugin') . '</p>';
|
|
}
|
|
|
|
$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();
|
|
?>
|
|
<div class="ansico-stat-chart-wrap ansico-stat-chart-wrap-enhanced <?php echo $is_widget ? 'ansico-stat-chart-wrap-widget' : ''; ?>" id="<?php echo esc_attr($chart_id); ?>-wrap">
|
|
<div class="ansico-stat-line-chart ansico-stat-line-chart-enhanced <?php echo $is_widget ? 'ansico-stat-line-chart-widget' : ''; ?>" id="<?php echo esc_attr($chart_id); ?>">
|
|
<svg viewBox="0 0 <?php echo esc_attr((string) $width); ?> <?php echo esc_attr((string) $height); ?>" width="100%" height="<?php echo esc_attr((string) $height); ?>" role="img" aria-label="<?php echo esc_attr__('Views chart', 'ansico-stat-plugin'); ?>">
|
|
<defs>
|
|
<linearGradient id="<?php echo esc_attr($chart_id . '-gradient'); ?>" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stop-color="#2271b1" stop-opacity="0.24"></stop>
|
|
<stop offset="100%" stop-color="#2271b1" stop-opacity="0.03"></stop>
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<?php foreach ($y_ticks as $tick) : ?>
|
|
<line x1="<?php echo esc_attr((string) $chart_left); ?>" y1="<?php echo esc_attr((string) $tick['y']); ?>" x2="<?php echo esc_attr((string) $chart_right); ?>" y2="<?php echo esc_attr((string) $tick['y']); ?>" stroke="#e5e7eb" stroke-width="1"></line>
|
|
<text x="0" y="<?php echo esc_attr((string) ($tick['y'] + 4)); ?>" font-size="<?php echo esc_attr($is_widget ? '13' : '11'); ?>" fill="#6b7280"><?php echo esc_html(number_format_i18n((int) $tick['value'])); ?></text>
|
|
<?php endforeach; ?>
|
|
|
|
<line x1="<?php echo esc_attr((string) $chart_left); ?>" y1="<?php echo esc_attr((string) $chart_bottom); ?>" x2="<?php echo esc_attr((string) $chart_right); ?>" y2="<?php echo esc_attr((string) $chart_bottom); ?>" stroke="#cbd5e1" stroke-width="1.2"></line>
|
|
|
|
<polygon fill="url(#<?php echo esc_attr($chart_id . '-gradient'); ?>)" points="<?php echo esc_attr($area_path); ?>"></polygon>
|
|
<polyline fill="none" stroke="#2271b1" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" points="<?php echo esc_attr(implode(' ', $points)); ?>"></polyline>
|
|
|
|
<?php foreach ($circles as $point) : ?>
|
|
<rect class="ansico-stat-chart-point-hit" x="<?php echo esc_attr((string) ($point['x'] - 18)); ?>" y="<?php echo esc_attr((string) ($point['y'] - 18)); ?>" width="36" height="36" rx="18" ry="18" fill="transparent" data-label="<?php echo esc_attr($point['label']); ?>" data-value="<?php echo esc_attr(number_format_i18n($point['value'])); ?>"></rect>
|
|
<circle class="ansico-stat-chart-point" cx="<?php echo esc_attr((string) $point['x']); ?>" cy="<?php echo esc_attr((string) $point['y']); ?>" r="3.5" fill="#ffffff" stroke="#2271b1" stroke-width="2.5"></circle>
|
|
<?php endforeach; ?>
|
|
|
|
<?php
|
|
$label_every = max(1, (int) ceil($count / 8));
|
|
foreach ($circles as $index => $point) :
|
|
if ($index % $label_every !== 0 && $index !== ($count - 1)) {
|
|
continue;
|
|
}
|
|
?>
|
|
<text x="<?php echo esc_attr((string) $point['x']); ?>" y="<?php echo esc_attr((string) ($is_widget ? 226 : 205)); ?>" text-anchor="middle" font-size="<?php echo esc_attr($is_widget ? '13' : '11'); ?>" fill="#6b7280"><?php echo esc_html($point['label']); ?></text>
|
|
<?php endforeach; ?>
|
|
</svg>
|
|
</div>
|
|
<div class="ansico-stat-chart-tooltip" style="display:none; left:-9999px; top:-9999px; opacity:0;" aria-hidden="true"></div>
|
|
</div>
|
|
<script>
|
|
(function() {
|
|
var root = document.getElementById(<?php echo wp_json_encode($chart_id); ?>);
|
|
var wrap = document.getElementById(<?php echo wp_json_encode($chart_id . '-wrap'); ?>);
|
|
if (!root || !wrap) return;
|
|
var tooltip = wrap.querySelector('.ansico-stat-chart-tooltip');
|
|
var points = root.querySelectorAll('.ansico-stat-chart-point-hit');
|
|
if (!tooltip || !points.length) return;
|
|
|
|
function resetTooltip() {
|
|
tooltip.style.display = 'none';
|
|
tooltip.style.opacity = '0';
|
|
tooltip.style.left = '-9999px';
|
|
tooltip.style.top = '-9999px';
|
|
}
|
|
|
|
function positionTooltipForPoint(point) {
|
|
var wrapRect = wrap.getBoundingClientRect();
|
|
var pointRect = point.getBoundingClientRect();
|
|
var tipRect = tooltip.getBoundingClientRect();
|
|
|
|
var pointCenterX = (pointRect.left - wrapRect.left) + (pointRect.width / 2);
|
|
var pointTopY = (pointRect.top - wrapRect.top);
|
|
|
|
var left = pointCenterX - (tipRect.width / 2);
|
|
var top = pointTopY - tipRect.height - 10;
|
|
|
|
if (left + tipRect.width > wrapRect.width - 8) left = wrapRect.width - tipRect.width - 8;
|
|
if (left < 8) left = 8;
|
|
if (top < 8) top = pointTopY + 18;
|
|
|
|
tooltip.style.left = left + 'px';
|
|
tooltip.style.top = top + 'px';
|
|
}
|
|
|
|
function showTooltip(event) {
|
|
var point = event.currentTarget;
|
|
tooltip.textContent = point.getAttribute('data-label') + ': ' + point.getAttribute('data-value');
|
|
tooltip.style.display = 'block';
|
|
tooltip.style.opacity = '1';
|
|
positionTooltipForPoint(point);
|
|
}
|
|
|
|
points.forEach(function(point) {
|
|
point.addEventListener('mouseenter', showTooltip);
|
|
point.addEventListener('mouseleave', resetTooltip);
|
|
});
|
|
|
|
wrap.addEventListener('mouseleave', resetTooltip);
|
|
resetTooltip();
|
|
})();
|
|
</script>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
protected function render_bar_chart_markup(array $chart_data, string $chart_id, string $aria_label = '') {
|
|
if ($aria_label === '') {
|
|
$aria_label = __('Chart', 'ansico-stat-plugin');
|
|
}
|
|
|
|
$values = [];
|
|
foreach ($chart_data as $row) {
|
|
$values[] = (int) ($row['views'] ?? $row['total_views'] ?? 0);
|
|
}
|
|
|
|
$max = max($values ?: [0]);
|
|
$sum = array_sum($values);
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="ansico-stat-bar-chart ansico-stat-bar-chart-wrap" id="<?php echo esc_attr($chart_id); ?>">
|
|
<?php if (empty($chart_data) || $sum <= 0) : ?>
|
|
<p><?php echo esc_html__('No tracked data for this period.', 'ansico-stat-plugin'); ?></p>
|
|
<?php else : ?>
|
|
<div class="ansico-stat-bar-chart-grid">
|
|
<?php foreach ($chart_data as $row) :
|
|
$value = (int) ($row['views'] ?? $row['total_views'] ?? 0);
|
|
$label = (string) ($row['label'] ?? $row['date'] ?? $row['dimension_label'] ?? $row['dimension_value'] ?? '');
|
|
$height = ($max > 0 && $value > 0) ? max(6, (int) round(($value / $max) * 140)) : 0;
|
|
?>
|
|
<div class="ansico-stat-bar-item">
|
|
<div class="ansico-stat-bar" style="height:<?php echo esc_attr((string) $height); ?>px;" data-label="<?php echo esc_attr($label); ?>" data-value="<?php echo esc_attr(number_format_i18n($value)); ?>"></div>
|
|
<div class="ansico-stat-bar-value"><?php echo esc_html(number_format_i18n($value)); ?></div>
|
|
<div class="ansico-stat-bar-label"><?php echo esc_html($label); ?></div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<div class="ansico-stat-chart-tooltip" style="display:none; left:-9999px; top:-9999px; opacity:0;" aria-hidden="true"></div>
|
|
<script>
|
|
(function() {
|
|
var root = document.getElementById(<?php echo wp_json_encode($chart_id); ?>);
|
|
if (!root) return;
|
|
var tooltip = root.querySelector('.ansico-stat-chart-tooltip');
|
|
var items = root.querySelectorAll('.ansico-stat-bar');
|
|
if (!tooltip || !items.length) return;
|
|
|
|
function resetTooltip() {
|
|
tooltip.style.display = 'none';
|
|
tooltip.style.opacity = '0';
|
|
tooltip.style.left = '-9999px';
|
|
tooltip.style.top = '-9999px';
|
|
}
|
|
|
|
function positionTooltipForBar(item) {
|
|
var rootRect = root.getBoundingClientRect();
|
|
var itemRect = item.getBoundingClientRect();
|
|
var tipRect = tooltip.getBoundingClientRect();
|
|
|
|
var centerX = (itemRect.left - rootRect.left) + (itemRect.width / 2);
|
|
var topY = (itemRect.top - rootRect.top);
|
|
|
|
var left = centerX - (tipRect.width / 2);
|
|
var top = topY - tipRect.height - 10;
|
|
|
|
if (left + tipRect.width > rootRect.width - 8) left = rootRect.width - tipRect.width - 8;
|
|
if (left < 8) left = 8;
|
|
if (top < 8) top = topY + 18;
|
|
|
|
tooltip.style.left = left + 'px';
|
|
tooltip.style.top = top + 'px';
|
|
}
|
|
|
|
function showTooltip(event) {
|
|
var item = event.currentTarget;
|
|
tooltip.textContent = item.getAttribute('data-label') + ': ' + item.getAttribute('data-value');
|
|
tooltip.style.display = 'block';
|
|
tooltip.style.opacity = '1';
|
|
positionTooltipForBar(item);
|
|
}
|
|
|
|
items.forEach(function(item) {
|
|
item.addEventListener('mouseenter', showTooltip);
|
|
item.addEventListener('mouseleave', resetTooltip);
|
|
});
|
|
|
|
root.addEventListener('mouseleave', resetTooltip);
|
|
resetTooltip();
|
|
})();
|
|
</script>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
protected function render_pie_chart_markup(array $segments, string $chart_id, string $aria_label = '') {
|
|
if ($aria_label === '') {
|
|
$aria_label = __('Pie chart', 'ansico-stat-plugin');
|
|
}
|
|
|
|
$filtered = [];
|
|
foreach ($segments as $label => $value) {
|
|
$value = (int) $value;
|
|
if ($value > 0) {
|
|
$filtered[(string) $label] = $value;
|
|
}
|
|
}
|
|
|
|
if (empty($filtered)) {
|
|
return '<p>' . esc_html__('No tracked data for this period.', 'ansico-stat-plugin') . '</p>';
|
|
}
|
|
|
|
$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();
|
|
?>
|
|
<div class="ansico-stat-pie-wrap" id="<?php echo esc_attr($chart_id); ?>">
|
|
<div class="ansico-stat-pie-chart-shell">
|
|
<svg viewBox="0 0 260 180" width="100%" height="180" role="img" aria-label="<?php echo esc_attr($aria_label); ?>">
|
|
<?php foreach ($filtered as $label => $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%';
|
|
?>
|
|
<path class="ansico-stat-pie-segment" d="M <?php echo esc_attr((string) $cx); ?> <?php echo esc_attr((string) $cy); ?> L <?php echo esc_attr(round($x1, 3)); ?> <?php echo esc_attr(round($y1, 3)); ?> A <?php echo esc_attr((string) $r); ?> <?php echo esc_attr((string) $r); ?> 0 <?php echo esc_attr((string) $largeArc); ?> 1 <?php echo esc_attr(round($x2, 3)); ?> <?php echo esc_attr(round($y2, 3)); ?> Z" fill="<?php echo esc_attr($color); ?>" data-label="<?php echo esc_attr($label); ?>" data-value="<?php echo esc_attr(number_format_i18n($value)); ?>" data-percent="<?php echo esc_attr($percent); ?>"></path>
|
|
<?php
|
|
$startAngle = $endAngle;
|
|
$index++;
|
|
endforeach; ?>
|
|
<circle cx="<?php echo esc_attr((string) $cx); ?>" cy="<?php echo esc_attr((string) $cy); ?>" r="32" fill="#fff"></circle>
|
|
<text x="<?php echo esc_attr((string) $cx); ?>" y="<?php echo esc_attr((string) ($cy + 4)); ?>" text-anchor="middle" font-size="12"><?php echo esc_html(number_format_i18n($total)); ?></text>
|
|
</svg>
|
|
<div class="ansico-stat-chart-tooltip" style="display:none; left:-9999px; top:-9999px; opacity:0;" aria-hidden="true"></div>
|
|
</div>
|
|
<div class="ansico-stat-pie-legend">
|
|
<?php $index = 0; foreach ($filtered as $label => $value) : $color = $palette[$index % count($palette)]; $percent = $total > 0 ? number_format_i18n(($value / $total) * 100, 1) . '%' : '0%'; ?>
|
|
<div class="ansico-stat-legend-item">
|
|
<span class="ansico-stat-legend-swatch" style="background:<?php echo esc_attr($color); ?>"></span>
|
|
<span><?php echo esc_html($label); ?> (<?php echo esc_html(number_format_i18n($value)); ?> · <?php echo esc_html($percent); ?>)</span>
|
|
</div>
|
|
<?php $index++; endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function() {
|
|
var root = document.getElementById(<?php echo wp_json_encode($chart_id); ?>);
|
|
if (!root) return;
|
|
var tooltip = root.querySelector('.ansico-stat-chart-tooltip');
|
|
var segments = root.querySelectorAll('.ansico-stat-pie-segment');
|
|
var host = root.querySelector('.ansico-stat-pie-chart-shell');
|
|
if (!tooltip || !segments.length || !host) return;
|
|
|
|
function resetTooltip() {
|
|
tooltip.style.display = 'none';
|
|
tooltip.style.opacity = '0';
|
|
tooltip.style.left = '-9999px';
|
|
tooltip.style.top = '-9999px';
|
|
}
|
|
|
|
function positionTooltip(event) {
|
|
var rect = host.getBoundingClientRect();
|
|
var tipRect = tooltip.getBoundingClientRect();
|
|
var x = event.clientX - rect.left;
|
|
var y = event.clientY - rect.top;
|
|
|
|
var left = x - (tipRect.width / 2);
|
|
var top = y - tipRect.height - 12;
|
|
|
|
if (left + tipRect.width > rect.width - 8) left = rect.width - tipRect.width - 8;
|
|
if (left < 8) left = 8;
|
|
if (top < 8) top = y + 16;
|
|
|
|
tooltip.style.left = left + 'px';
|
|
tooltip.style.top = top + 'px';
|
|
}
|
|
|
|
function showTooltip(event) {
|
|
var segment = event.currentTarget;
|
|
tooltip.textContent = segment.getAttribute('data-label') + ': ' + segment.getAttribute('data-value') + ' (' + segment.getAttribute('data-percent') + ')';
|
|
tooltip.style.display = 'block';
|
|
tooltip.style.opacity = '1';
|
|
positionTooltip(event);
|
|
}
|
|
|
|
segments.forEach(function(segment) {
|
|
segment.addEventListener('mouseenter', showTooltip);
|
|
segment.addEventListener('mousemove', function(event) {
|
|
if (tooltip.style.display === 'none') return;
|
|
positionTooltip(event);
|
|
});
|
|
segment.addEventListener('mouseleave', resetTooltip);
|
|
});
|
|
|
|
host.addEventListener('mouseleave', resetTooltip);
|
|
resetTooltip();
|
|
})();
|
|
</script>
|
|
<?php
|
|
return ob_get_clean();
|
|
}
|
|
|
|
|
|
protected function date_days_ago(int $days) {
|
|
return wp_date('Y-m-d', strtotime('-' . max(0, $days) . ' days', current_time('timestamp')));
|
|
}
|
|
|
|
protected function get_daily_chart_data(int $days = 30) {
|
|
global $wpdb;
|
|
$days = max(1, $days);
|
|
$daily_table = self::daily_table_name();
|
|
$start = $this->date_days_ago($days - 1);
|
|
$end = current_time('Y-m-d');
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT stat_date, views FROM {$daily_table} WHERE stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
|
|
$indexed = [];
|
|
foreach ($rows as $row) {
|
|
$indexed[$row['stat_date']] = (int) $row['views'];
|
|
}
|
|
|
|
$data = [];
|
|
$cursor = strtotime($start);
|
|
$end_ts = strtotime($end);
|
|
while ($cursor <= $end_ts) {
|
|
$date = gmdate('Y-m-d', $cursor);
|
|
$data[] = [
|
|
'date' => $date,
|
|
'views' => $indexed[$date] ?? 0,
|
|
];
|
|
$cursor = strtotime('+1 day', $cursor);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function get_install_month_key() {
|
|
$install_date = get_option(self::INSTALL_OPTION_KEY, current_time('Y-m-d'));
|
|
$timestamp = strtotime($install_date ?: current_time('Y-m-d'));
|
|
return wp_date('Y-m', $timestamp ?: current_time('timestamp'));
|
|
}
|
|
|
|
protected function get_install_year() {
|
|
$install_date = get_option(self::INSTALL_OPTION_KEY, current_time('Y-m-d'));
|
|
$timestamp = strtotime($install_date ?: current_time('Y-m-d'));
|
|
return (int) wp_date('Y', $timestamp ?: current_time('timestamp'));
|
|
}
|
|
|
|
protected function get_selected_month() {
|
|
$requested = isset($_GET['month']) ? sanitize_text_field(wp_unslash($_GET['month'])) : '';
|
|
if ($requested && preg_match('/^\d{4}-\d{2}$/', $requested)) {
|
|
return $requested;
|
|
}
|
|
return wp_date('Y-m', current_time('timestamp'));
|
|
}
|
|
|
|
protected function get_selected_year() {
|
|
$requested = isset($_GET['year']) ? absint(wp_unslash($_GET['year'])) : 0;
|
|
if ($requested >= 1970 && $requested <= 9999) {
|
|
return $requested;
|
|
}
|
|
return (int) wp_date('Y', current_time('timestamp'));
|
|
}
|
|
|
|
protected function render_month_navigation(string $selected_month) {
|
|
$selected_ts = strtotime($selected_month . '-01');
|
|
$current_ts = strtotime(wp_date('Y-m-01', current_time('timestamp')));
|
|
$prev_month = wp_date('Y-m', strtotime('-1 month', $selected_ts));
|
|
$next_month = wp_date('Y-m', strtotime('+1 month', $selected_ts));
|
|
$base_url = admin_url('admin.php?page=' . self::MENU_SLUG_STATS);
|
|
|
|
echo '<div class="ansico-stat-nav" style="display:flex;justify-content:space-between;align-items:center;gap:12px;margin:0 0 1rem;">';
|
|
echo '<a class="button button-secondary" href="' . esc_url(add_query_arg(['month' => $prev_month, 'year' => (int) wp_date('Y', $selected_ts)], $base_url)) . '">← ' . esc_html__('Older month', 'ansico-stat-plugin') . '</a>';
|
|
echo '<strong>' . esc_html(wp_date('F Y', $selected_ts)) . '</strong>';
|
|
if ($selected_ts < $current_ts) {
|
|
echo '<a class="button button-secondary" href="' . esc_url(add_query_arg(['month' => $next_month, 'year' => (int) wp_date('Y', strtotime($next_month . '-01'))], $base_url)) . '">' . esc_html__('Newer month', 'ansico-stat-plugin') . ' →</a>';
|
|
} else {
|
|
echo '<span></span>';
|
|
}
|
|
echo '</div>';
|
|
}
|
|
|
|
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 '<div class="ansico-stat-nav" style="display:flex;justify-content:space-between;align-items:center;gap:12px;margin:0 0 1rem;">';
|
|
echo '<a class="button button-secondary" href="' . esc_url(add_query_arg(['year' => $selected_year - 1, 'month' => $this->get_selected_month()], $base_url)) . '">← ' . esc_html__('Older year', 'ansico-stat-plugin') . '</a>';
|
|
echo '<strong>' . esc_html((string) $selected_year) . '</strong>';
|
|
if ($selected_year < $current_year) {
|
|
echo '<a class="button button-secondary" href="' . esc_url(add_query_arg(['year' => $selected_year + 1, 'month' => $this->get_selected_month()], $base_url)) . '">' . esc_html__('Newer year', 'ansico-stat-plugin') . ' →</a>';
|
|
} else {
|
|
echo '<span></span>';
|
|
}
|
|
echo '</div>';
|
|
}
|
|
|
|
protected function get_top_posts_in_period(string $start, string $end, int $limit = 10) {
|
|
global $wpdb;
|
|
$table = self::post_daily_table_name();
|
|
|
|
$results = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT post_id, SUM(views) AS period_views
|
|
FROM {$table}
|
|
WHERE stat_date BETWEEN %s AND %s
|
|
GROUP BY post_id
|
|
ORDER BY period_views DESC
|
|
LIMIT %d",
|
|
$start,
|
|
$end,
|
|
$limit
|
|
), ARRAY_A);
|
|
|
|
return is_array($results) ? $results : [];
|
|
}
|
|
|
|
protected function get_monthly_top_posts(string $month_key, int $limit = 10) {
|
|
$start = $month_key . '-01';
|
|
$end = wp_date('Y-m-t', strtotime($start));
|
|
return $this->get_top_posts_in_period($start, $end, $limit);
|
|
}
|
|
|
|
protected function get_yearly_top_posts(int $year, int $limit = 10) {
|
|
$start = sprintf('%04d-01-01', $year);
|
|
$end = sprintf('%04d-12-31', $year);
|
|
return $this->get_top_posts_in_period($start, $end, $limit);
|
|
}
|
|
|
|
protected function get_latest_month_range() {
|
|
return [
|
|
'start' => wp_date('Y-m-01', current_time('timestamp')),
|
|
'end' => wp_date('Y-m-t', current_time('timestamp')),
|
|
];
|
|
}
|
|
|
|
protected function get_post_views_for_latest_month(int $post_id) {
|
|
global $wpdb;
|
|
$range = $this->get_latest_month_range();
|
|
$table = self::post_daily_table_name();
|
|
|
|
$views = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT SUM(views)
|
|
FROM {$table}
|
|
WHERE post_id = %d
|
|
AND stat_date BETWEEN %s AND %s",
|
|
$post_id,
|
|
$range['start'],
|
|
$range['end']
|
|
));
|
|
|
|
return (int) $views;
|
|
}
|
|
|
|
protected function get_referral_summary_for_month(string $month_key) {
|
|
global $wpdb;
|
|
$table = self::referral_table_name();
|
|
$start = $month_key . '-01';
|
|
$end = wp_date('Y-m-t', strtotime($start));
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT category, SUM(visits) AS total_visits
|
|
FROM {$table}
|
|
WHERE stat_date BETWEEN %s AND %s
|
|
GROUP BY category",
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
|
|
$summary = [
|
|
'direct' => 0,
|
|
'search' => 0,
|
|
'social' => 0,
|
|
'website' => 0,
|
|
];
|
|
|
|
foreach ($rows as $row) {
|
|
$category = isset($row['category']) ? (string) $row['category'] : '';
|
|
if (isset($summary[$category])) {
|
|
$summary[$category] = (int) $row['total_visits'];
|
|
}
|
|
}
|
|
|
|
return $summary;
|
|
}
|
|
|
|
protected function get_referral_sources_for_month(string $month_key, string $category, int $limit = 50) {
|
|
global $wpdb;
|
|
$table = self::referral_table_name();
|
|
$start = $month_key . '-01';
|
|
$end = wp_date('Y-m-t', strtotime($start));
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT source_url, source_host, SUM(visits) AS total_visits
|
|
FROM {$table}
|
|
WHERE stat_date BETWEEN %s AND %s
|
|
AND category = %s
|
|
AND source_url <> ''
|
|
GROUP BY source_url, source_host
|
|
ORDER BY total_visits DESC, source_host ASC
|
|
LIMIT %d",
|
|
$start,
|
|
$end,
|
|
$category,
|
|
$limit
|
|
), ARRAY_A);
|
|
|
|
return is_array($rows) ? $rows : [];
|
|
}
|
|
|
|
protected function get_top_404_pages_for_month(string $month_key, int $limit = 10) {
|
|
global $wpdb;
|
|
$table = self::page_daily_table_name();
|
|
$start = $month_key . '-01';
|
|
$end = wp_date('Y-m-t', strtotime($start));
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT page_key, page_label, page_url, SUM(views) AS period_views
|
|
FROM {$table}
|
|
WHERE stat_date BETWEEN %s AND %s
|
|
AND page_type = %s
|
|
GROUP BY page_key, page_label, page_url
|
|
ORDER BY period_views DESC, page_url ASC
|
|
LIMIT %d",
|
|
$start,
|
|
$end,
|
|
'404',
|
|
$limit
|
|
), ARRAY_A);
|
|
|
|
return is_array($rows) ? $rows : [];
|
|
}
|
|
|
|
|
|
protected function get_dimension_rows_for_month(string $month_key, string $dimension_type, int $limit = 10) {
|
|
global $wpdb;
|
|
$table = self::dimension_table_name();
|
|
$start = $month_key . '-01';
|
|
$end = wp_date('Y-m-t', strtotime($start));
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT dimension_value, SUM(views) AS total_views
|
|
FROM {$table}
|
|
WHERE stat_date BETWEEN %s AND %s
|
|
AND dimension_type = %s
|
|
GROUP BY dimension_value
|
|
ORDER BY total_views DESC, dimension_value ASC
|
|
LIMIT %d",
|
|
$start,
|
|
$end,
|
|
$dimension_type,
|
|
$limit
|
|
), ARRAY_A);
|
|
|
|
if (!is_array($rows)) {
|
|
return [];
|
|
}
|
|
|
|
if ($dimension_type === 'country') {
|
|
foreach ($rows as &$row) {
|
|
$row['dimension_label'] = $this->get_country_label((string) ($row['dimension_value'] ?? ''));
|
|
}
|
|
unset($row);
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
protected function get_dimension_rows_for_year(int $year, string $dimension_type, int $limit = 10) {
|
|
global $wpdb;
|
|
$table = self::dimension_table_name();
|
|
$start = sprintf('%04d-01-01', $year);
|
|
$end = sprintf('%04d-12-31', $year);
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT dimension_value, SUM(views) AS total_views
|
|
FROM {$table}
|
|
WHERE stat_date BETWEEN %s AND %s
|
|
AND dimension_type = %s
|
|
GROUP BY dimension_value
|
|
ORDER BY total_views DESC, dimension_value ASC
|
|
LIMIT %d",
|
|
$start,
|
|
$end,
|
|
$dimension_type,
|
|
$limit
|
|
), ARRAY_A);
|
|
|
|
if (!is_array($rows)) {
|
|
return [];
|
|
}
|
|
|
|
if ($dimension_type === 'country') {
|
|
foreach ($rows as &$row) {
|
|
$row['dimension_label'] = $this->get_country_label((string) ($row['dimension_value'] ?? ''));
|
|
}
|
|
unset($row);
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
protected function get_referral_summary_for_year(int $year) {
|
|
global $wpdb;
|
|
$table = self::referral_table_name();
|
|
$start = sprintf('%04d-01-01', $year);
|
|
$end = sprintf('%04d-12-31', $year);
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT category, SUM(visits) AS total_visits
|
|
FROM {$table}
|
|
WHERE stat_date BETWEEN %s AND %s
|
|
GROUP BY category",
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
|
|
$summary = [
|
|
'Direct' => 0,
|
|
'Search' => 0,
|
|
'Social' => 0,
|
|
'Other websites' => 0,
|
|
];
|
|
|
|
foreach ($rows as $row) {
|
|
$category = (string) ($row['category'] ?? '');
|
|
$value = (int) ($row['total_visits'] ?? 0);
|
|
if ($category === 'direct') {
|
|
$summary['Direct'] = $value;
|
|
} elseif ($category === 'search') {
|
|
$summary['Search'] = $value;
|
|
} elseif ($category === 'social') {
|
|
$summary['Social'] = $value;
|
|
} elseif ($category === 'website') {
|
|
$summary['Other websites'] = $value;
|
|
}
|
|
}
|
|
|
|
return $summary;
|
|
}
|
|
|
|
protected function get_monthly_totals_for_year(int $year) {
|
|
global $wpdb;
|
|
$table = self::daily_table_name();
|
|
$start = sprintf('%04d-01-01', $year);
|
|
$end = sprintf('%04d-12-31', $year);
|
|
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT stat_date, views
|
|
FROM {$table}
|
|
WHERE stat_date BETWEEN %s AND %s
|
|
ORDER BY stat_date ASC",
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
|
|
$map = [];
|
|
for ($month = 1; $month <= 12; $month++) {
|
|
$month_key = sprintf('%04d-%02d', $year, $month);
|
|
$map[$month_key] = 0;
|
|
}
|
|
|
|
if (is_array($rows)) {
|
|
foreach ($rows as $row) {
|
|
$stat_date = isset($row['stat_date']) ? (string) $row['stat_date'] : '';
|
|
$month_key = substr($stat_date, 0, 7);
|
|
if ($month_key !== '' && isset($map[$month_key])) {
|
|
$map[$month_key] += (int) ($row['views'] ?? 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
$data = [];
|
|
foreach ($map as $month_key => $views) {
|
|
$data[] = [
|
|
'label' => wp_date('M', strtotime($month_key . '-01')),
|
|
'views' => (int) $views,
|
|
];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
|
|
|
|
protected function is_unknown_only_dimension_rows(array $rows) {
|
|
if (empty($rows)) {
|
|
return true;
|
|
}
|
|
|
|
foreach ($rows as $row) {
|
|
$value = strtolower(trim((string) ($row['dimension_value'] ?? '')));
|
|
$label = strtolower(trim((string) ($row['dimension_label'] ?? '')));
|
|
if (!in_array($value, ['', 'unknown'], true) && !in_array($label, ['', 'unknown'], true)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function format_percent(int $value, int $total) {
|
|
if ($total <= 0) {
|
|
return '0%';
|
|
}
|
|
|
|
$percent = ($value / $total) * 100;
|
|
return number_format_i18n($percent, 1) . '%';
|
|
}
|
|
|
|
protected function render_referral_summary_table(array $summary) {
|
|
$total = array_sum(array_map('intval', $summary));
|
|
?>
|
|
<table class="widefat striped ansico-stat-table">
|
|
<thead>
|
|
<tr>
|
|
<th><?php echo esc_html__('Referral type', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('Visits', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('Percent', 'ansico-stat-plugin'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><?php echo esc_html__('Direct visitors', 'ansico-stat-plugin'); ?></td><td><?php echo esc_html(number_format_i18n((int) ($summary['direct'] ?? 0))); ?></td><td><?php echo esc_html($this->format_percent((int) ($summary['direct'] ?? 0), $total)); ?></td></tr>
|
|
<tr><td><?php echo esc_html__('Visitors from search engines', 'ansico-stat-plugin'); ?></td><td><?php echo esc_html(number_format_i18n((int) ($summary['search'] ?? 0))); ?></td><td><?php echo esc_html($this->format_percent((int) ($summary['search'] ?? 0), $total)); ?></td></tr>
|
|
<tr><td><?php echo esc_html__('Visitors from social media', 'ansico-stat-plugin'); ?></td><td><?php echo esc_html(number_format_i18n((int) ($summary['social'] ?? 0))); ?></td><td><?php echo esc_html($this->format_percent((int) ($summary['social'] ?? 0), $total)); ?></td></tr>
|
|
<tr><td><?php echo esc_html__('Visitors from other websites', 'ansico-stat-plugin'); ?></td><td><?php echo esc_html(number_format_i18n((int) ($summary['website'] ?? 0))); ?></td><td><?php echo esc_html($this->format_percent((int) ($summary['website'] ?? 0), $total)); ?></td></tr>
|
|
</tbody>
|
|
</table>
|
|
<?php
|
|
}
|
|
|
|
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 = 0;
|
|
foreach ($rows as $row) {
|
|
$total += (int) ($row['total_views'] ?? 0);
|
|
}
|
|
?>
|
|
<table class="widefat striped ansico-stat-table">
|
|
<thead>
|
|
<tr>
|
|
<th><?php echo esc_html__('#', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html($value_label); ?></th>
|
|
<th><?php echo esc_html($views_label); ?></th>
|
|
<th><?php echo esc_html__('Percent', 'ansico-stat-plugin'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($rows as $index => $row) : ?>
|
|
<?php $count = (int) ($row['total_views'] ?? 0); ?>
|
|
<tr>
|
|
<td><?php echo esc_html((string) ($index + 1)); ?></td>
|
|
<td>
|
|
<?php
|
|
$label = (string) ($row['dimension_label'] ?? $row['dimension_value'] ?? '');
|
|
$value = (string) ($row['dimension_value'] ?? '');
|
|
echo esc_html($label);
|
|
if (!empty($value) && $label !== $value) {
|
|
echo ' <span class="description">(' . esc_html($value) . ')</span>';
|
|
}
|
|
?>
|
|
</td>
|
|
<td><?php echo esc_html(number_format_i18n($count)); ?></td>
|
|
<td><?php echo esc_html($this->format_percent($count, $total)); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php
|
|
}
|
|
|
|
protected function render_top_404_table(array $rows) {
|
|
if (empty($rows)) {
|
|
echo '<p>' . esc_html__('No tracked 404 views for this month.', 'ansico-stat-plugin') . '</p>';
|
|
return;
|
|
}
|
|
?>
|
|
<table class="widefat striped ansico-stat-table">
|
|
<thead>
|
|
<tr>
|
|
<th><?php echo esc_html__('#', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('404 URL', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('Views', 'ansico-stat-plugin'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($rows as $index => $row) : ?>
|
|
<tr>
|
|
<td><?php echo esc_html((string) ($index + 1)); ?></td>
|
|
<td><?php echo $this->format_external_link((string) ($row['page_url'] ?? '')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
|
|
<td><?php echo esc_html(number_format_i18n((int) ($row['period_views'] ?? 0))); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php
|
|
}
|
|
|
|
protected function render_top_posts_table(array $rows, string $views_label) {
|
|
if (empty($rows)) {
|
|
echo '<p>' . esc_html__('No views tracked for this period.', 'ansico-stat-plugin') . '</p>';
|
|
return;
|
|
}
|
|
?>
|
|
<table class="widefat striped ansico-stat-table">
|
|
<thead>
|
|
<tr>
|
|
<th><?php echo esc_html__('#', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('Title', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('Post type', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html($views_label); ?></th>
|
|
<th><?php echo esc_html__('Total lifetime views', 'ansico-stat-plugin'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($rows as $index => $row) : ?>
|
|
<?php
|
|
$post_id = (int) $row['post_id'];
|
|
$title = get_the_title($post_id) ?: __('(no title)', 'ansico-stat-plugin');
|
|
$total_views = (int) get_post_meta($post_id, self::TOTAL_META_KEY, true);
|
|
$post_type = get_post_type($post_id);
|
|
?>
|
|
<tr>
|
|
<td><?php echo esc_html((string) ($index + 1)); ?></td>
|
|
<td><?php echo $this->format_post_link($post_id, $title); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
|
|
<td><?php echo esc_html($post_type ?: '—'); ?></td>
|
|
<td><?php echo esc_html(number_format_i18n((int) $row['period_views'])); ?></td>
|
|
<td><?php echo esc_html(number_format_i18n($total_views)); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php
|
|
}
|
|
|
|
protected function render_referral_sources_table(array $rows, string $label) {
|
|
echo '<h3>' . esc_html($label) . '</h3>';
|
|
if (empty($rows)) {
|
|
echo '<p>' . esc_html__('No tracked referral URLs for this category in the selected month.', 'ansico-stat-plugin') . '</p>';
|
|
return;
|
|
}
|
|
?>
|
|
<?php $total = 0; foreach ($rows as $row) { $total += (int) ($row['total_visits'] ?? 0); } ?>
|
|
<table class="widefat striped ansico-stat-table">
|
|
<thead>
|
|
<tr>
|
|
<th><?php echo esc_html__('Source host', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('Source URL', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('Visits', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('Percent', 'ansico-stat-plugin'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($rows as $row) : ?>
|
|
<?php $count = (int) ($row['total_visits'] ?? 0); ?>
|
|
<tr>
|
|
<td><?php echo esc_html($row['source_host'] ?: '—'); ?></td>
|
|
<td><?php echo $this->format_external_link((string) $row['source_url']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
|
|
<td><?php echo esc_html(number_format_i18n($count)); ?></td>
|
|
<td><?php echo esc_html($this->format_percent($count, $total)); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php
|
|
}
|
|
|
|
|
|
protected function normalize_target_url(string $url) {
|
|
$url = trim($url);
|
|
if ($url === '') {
|
|
$url = home_url('/');
|
|
}
|
|
|
|
if (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) {
|
|
$url = home_url('/' . ltrim($url, '/'));
|
|
}
|
|
|
|
$parts = wp_parse_url($url);
|
|
if (empty($parts['host'])) {
|
|
return '';
|
|
}
|
|
|
|
$scheme = !empty($parts['scheme']) ? strtolower((string) $parts['scheme']) : (is_ssl() ? 'https' : 'http');
|
|
$host = strtolower((string) $parts['host']);
|
|
$path = isset($parts['path']) ? (string) $parts['path'] : '/';
|
|
$path = $path === '' ? '/' : $path;
|
|
$normalized_path = untrailingslashit($path);
|
|
$normalized_path = $normalized_path === '' ? '/' : $normalized_path;
|
|
$normalized = $scheme . '://' . $host . ($normalized_path === '/' ? '/' : $normalized_path . '/');
|
|
|
|
if (!empty($parts['query'])) {
|
|
$normalized .= '?' . $parts['query'];
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
protected function get_requested_single_stats_url() {
|
|
$requested = isset($_GET['target_url']) ? sanitize_text_field(wp_unslash($_GET['target_url'])) : '';
|
|
return $requested !== '' ? $requested : home_url('/');
|
|
}
|
|
|
|
protected function sanitize_stats_date(string $date) {
|
|
$date = trim($date);
|
|
if ($date === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
|
return '';
|
|
}
|
|
|
|
$timestamp = strtotime($date . ' 00:00:00');
|
|
if (!$timestamp) {
|
|
return '';
|
|
}
|
|
|
|
return gmdate('Y-m-d', $timestamp) === $date ? $date : '';
|
|
}
|
|
|
|
protected function get_single_stats_date_range() {
|
|
$default_end = current_time('Y-m-d');
|
|
$default_start = $this->date_days_ago(29);
|
|
|
|
$start = isset($_GET['start_date']) ? $this->sanitize_stats_date((string) wp_unslash($_GET['start_date'])) : '';
|
|
$end = isset($_GET['end_date']) ? $this->sanitize_stats_date((string) wp_unslash($_GET['end_date'])) : '';
|
|
|
|
if ($start === '') {
|
|
$start = $default_start;
|
|
}
|
|
if ($end === '') {
|
|
$end = $default_end;
|
|
}
|
|
if (strtotime($start) > strtotime($end)) {
|
|
[$start, $end] = [$end, $start];
|
|
}
|
|
|
|
return [
|
|
'start' => $start,
|
|
'end' => $end,
|
|
'days' => max(1, (int) floor((strtotime($end) - strtotime($start)) / DAY_IN_SECONDS) + 1),
|
|
'is_custom' => isset($_GET['start_date']) || isset($_GET['end_date']),
|
|
];
|
|
}
|
|
|
|
protected function build_single_stats_admin_url(string $target_url, string $start_date = '', string $end_date = '') {
|
|
$args = [
|
|
'page' => self::MENU_SLUG_SINGLE,
|
|
'target_url' => $target_url !== '' ? $target_url : home_url('/'),
|
|
];
|
|
if ($start_date !== '') {
|
|
$args['start_date'] = $start_date;
|
|
}
|
|
if ($end_date !== '') {
|
|
$args['end_date'] = $end_date;
|
|
}
|
|
return add_query_arg($args, admin_url('admin.php'));
|
|
}
|
|
|
|
public function register_frontend_admin_bar_link($wp_admin_bar) {
|
|
if (is_admin() || !is_admin_bar_showing() || !current_user_can('manage_options') || !$this->is_trackable_request()) {
|
|
return;
|
|
}
|
|
|
|
$page_context = $this->get_current_page_context();
|
|
if (empty($page_context) || empty($page_context['url'])) {
|
|
return;
|
|
}
|
|
|
|
$target_url = $this->normalize_target_url((string) $page_context['url']);
|
|
if ($target_url === '') {
|
|
return;
|
|
}
|
|
|
|
$wp_admin_bar->add_node([
|
|
'id' => 'ansico-stat-single-page',
|
|
'parent' => 'top-secondary',
|
|
'title' => '<span class="ab-icon dashicons dashicons-chart-bar" style="font:normal 20px/1 dashicons;top:2px;"></span><span class="ab-label">' . esc_html__('Page stats', 'ansico-stat-plugin') . '</span>',
|
|
'href' => esc_url($this->build_single_stats_admin_url($target_url)),
|
|
'meta' => [
|
|
'class' => 'ansico-stat-admin-bar-link',
|
|
'title' => esc_attr__('Open statistics for this page', 'ansico-stat-plugin'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
protected function resolve_single_stats_target(string $url) {
|
|
global $wpdb;
|
|
|
|
$normalized_url = $this->normalize_target_url($url);
|
|
if ($normalized_url === '') {
|
|
return [
|
|
'success' => false,
|
|
'message' => __('Please enter a valid URL.', 'ansico-stat-plugin'),
|
|
'requested_url' => $url,
|
|
'normalized_url' => '',
|
|
];
|
|
}
|
|
|
|
$site_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
|
|
$target_host = strtolower((string) wp_parse_url($normalized_url, PHP_URL_HOST));
|
|
if ($site_host === '' || $target_host === '' || preg_replace('/^www\./', '', $site_host) !== preg_replace('/^www\./', '', $target_host)) {
|
|
return [
|
|
'success' => false,
|
|
'message' => __('The URL must belong to this WordPress site.', 'ansico-stat-plugin'),
|
|
'requested_url' => $url,
|
|
'normalized_url' => $normalized_url,
|
|
];
|
|
}
|
|
|
|
$post_id = url_to_postid($normalized_url);
|
|
if ($post_id > 0) {
|
|
$post = get_post($post_id);
|
|
if ($this->is_trackable_post($post)) {
|
|
return [
|
|
'success' => true,
|
|
'requested_url' => $url,
|
|
'normalized_url' => $normalized_url,
|
|
'target' => [
|
|
'kind' => 'singular',
|
|
'post_id' => (int) $post_id,
|
|
'post_type' => sanitize_key((string) $post->post_type),
|
|
'page_key' => 'post:' . (int) $post_id,
|
|
'page_type' => sanitize_key((string) $post->post_type),
|
|
'object_id' => (int) $post_id,
|
|
'label' => get_the_title($post_id) ?: __('(no title)', 'ansico-stat-plugin'),
|
|
'url' => get_permalink($post_id) ?: $normalized_url,
|
|
'resolved_via' => 'post',
|
|
],
|
|
];
|
|
}
|
|
}
|
|
|
|
$front_url = $this->normalize_target_url(home_url('/'));
|
|
if ($normalized_url === $front_url) {
|
|
return [
|
|
'success' => true,
|
|
'requested_url' => $url,
|
|
'normalized_url' => $normalized_url,
|
|
'target' => [
|
|
'kind' => 'archive',
|
|
'page_key' => 'front_page',
|
|
'page_type' => 'front_page',
|
|
'object_id' => 0,
|
|
'label' => __('Front page', 'ansico-stat-plugin'),
|
|
'url' => home_url('/'),
|
|
'resolved_via' => 'front_page',
|
|
],
|
|
];
|
|
}
|
|
|
|
$posts_page_id = (int) get_option('page_for_posts');
|
|
if ($posts_page_id > 0) {
|
|
$posts_page_url = $this->normalize_target_url((string) get_permalink($posts_page_id));
|
|
if ($posts_page_url !== '' && $normalized_url === $posts_page_url) {
|
|
return [
|
|
'success' => true,
|
|
'requested_url' => $url,
|
|
'normalized_url' => $normalized_url,
|
|
'target' => [
|
|
'kind' => 'archive',
|
|
'page_key' => 'home',
|
|
'page_type' => 'home',
|
|
'object_id' => $posts_page_id,
|
|
'label' => __('Posts page', 'ansico-stat-plugin'),
|
|
'url' => get_permalink($posts_page_id) ?: $normalized_url,
|
|
'resolved_via' => 'posts_page',
|
|
],
|
|
];
|
|
}
|
|
}
|
|
|
|
$page_daily_table = self::page_daily_table_name();
|
|
$url_variants = array_values(array_unique(array_filter([
|
|
$normalized_url,
|
|
untrailingslashit($normalized_url),
|
|
trailingslashit($normalized_url),
|
|
])));
|
|
|
|
if (!empty($url_variants)) {
|
|
$placeholders = implode(',', array_fill(0, count($url_variants), '%s'));
|
|
$sql = "SELECT page_key, page_type, object_id, page_label, page_url\n FROM {$page_daily_table}\n WHERE page_url IN ({$placeholders})\n ORDER BY stat_date DESC\n LIMIT 1";
|
|
$row = $wpdb->get_row($wpdb->prepare($sql, $url_variants), ARRAY_A);
|
|
if (is_array($row) && !empty($row['page_key'])) {
|
|
return [
|
|
'success' => true,
|
|
'requested_url' => $url,
|
|
'normalized_url' => $normalized_url,
|
|
'target' => [
|
|
'kind' => 'archive',
|
|
'page_key' => (string) $row['page_key'],
|
|
'page_type' => sanitize_key((string) ($row['page_type'] ?? 'archive')),
|
|
'object_id' => !empty($row['object_id']) ? (int) $row['object_id'] : 0,
|
|
'label' => sanitize_text_field((string) ($row['page_label'] ?? $normalized_url)),
|
|
'url' => !empty($row['page_url']) ? esc_url_raw((string) $row['page_url']) : $normalized_url,
|
|
'resolved_via' => 'page_daily',
|
|
],
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => __('No tracked page or post was found for that URL yet. Try opening the page on the site first so the plugin can register it, and then update this report again.', 'ansico-stat-plugin'),
|
|
'requested_url' => $url,
|
|
'normalized_url' => $normalized_url,
|
|
];
|
|
}
|
|
|
|
protected function get_target_lifetime_views(array $target) {
|
|
global $wpdb;
|
|
|
|
if (($target['kind'] ?? '') === 'singular') {
|
|
return (int) get_post_meta((int) $target['post_id'], self::TOTAL_META_KEY, true);
|
|
}
|
|
|
|
$table = self::page_daily_table_name();
|
|
$views = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT SUM(views) FROM {$table} WHERE page_key = %s",
|
|
(string) $target['page_key']
|
|
));
|
|
|
|
return (int) $views;
|
|
}
|
|
|
|
|
|
protected function get_target_lifetime_unique_visitors(array $target) {
|
|
global $wpdb;
|
|
|
|
if (($target['kind'] ?? '') === 'singular') {
|
|
$table = self::post_daily_table_name();
|
|
$uniques = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT SUM(unique_visitors) FROM {$table} WHERE post_id = %d",
|
|
(int) $target['post_id']
|
|
));
|
|
return (int) $uniques;
|
|
}
|
|
|
|
$table = self::page_daily_table_name();
|
|
$uniques = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT SUM(unique_visitors) FROM {$table} WHERE page_key = %s",
|
|
(string) $target['page_key']
|
|
));
|
|
return (int) $uniques;
|
|
}
|
|
|
|
protected function get_target_period_unique_visitors(array $target, string $start, string $end) {
|
|
global $wpdb;
|
|
|
|
if (($target['kind'] ?? '') === 'singular') {
|
|
$table = self::post_daily_table_name();
|
|
$uniques = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT SUM(unique_visitors) FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s",
|
|
(int) $target['post_id'],
|
|
$start,
|
|
$end
|
|
));
|
|
return (int) $uniques;
|
|
}
|
|
|
|
$table = self::page_daily_table_name();
|
|
$uniques = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT SUM(unique_visitors) FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s",
|
|
(string) $target['page_key'],
|
|
$start,
|
|
$end
|
|
));
|
|
return (int) $uniques;
|
|
}
|
|
|
|
protected function get_target_daily_chart_data_between(array $target, string $start, string $end, string $metric = 'views') {
|
|
global $wpdb;
|
|
|
|
$metric = $metric === 'unique_visitors' ? 'unique_visitors' : 'views';
|
|
if (($target['kind'] ?? '') === 'singular') {
|
|
$table = self::post_daily_table_name();
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT stat_date, {$metric} AS metric_value FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
|
|
(int) $target['post_id'],
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
} else {
|
|
$table = self::page_daily_table_name();
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT stat_date, {$metric} AS metric_value FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
|
|
(string) $target['page_key'],
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
}
|
|
|
|
$indexed = [];
|
|
foreach ((array) $rows as $row) {
|
|
$indexed[(string) $row['stat_date']] = (int) $row['metric_value'];
|
|
}
|
|
|
|
$data = [];
|
|
$cursor = strtotime($start);
|
|
$end_ts = strtotime($end);
|
|
while ($cursor <= $end_ts) {
|
|
$date = gmdate('Y-m-d', $cursor);
|
|
$data[] = [
|
|
'date' => $date,
|
|
'views' => $indexed[$date] ?? 0,
|
|
];
|
|
$cursor = strtotime('+1 day', $cursor);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function get_target_monthly_chart_data_between(array $target, string $start, string $end) {
|
|
global $wpdb;
|
|
|
|
if (($target['kind'] ?? '') === 'singular') {
|
|
$table = self::post_daily_table_name();
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT DATE_FORMAT(stat_date, '%%Y-%%m') AS month_key, SUM(views) AS total_views FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s GROUP BY month_key ORDER BY month_key ASC",
|
|
(int) $target['post_id'],
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
} else {
|
|
$table = self::page_daily_table_name();
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT DATE_FORMAT(stat_date, '%%Y-%%m') AS month_key, SUM(views) AS total_views FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s GROUP BY month_key ORDER BY month_key ASC",
|
|
(string) $target['page_key'],
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
}
|
|
|
|
$indexed = [];
|
|
foreach ((array) $rows as $row) {
|
|
$indexed[(string) $row['month_key']] = (int) $row['total_views'];
|
|
}
|
|
|
|
$data = [];
|
|
$cursor = strtotime(gmdate('Y-m-01', strtotime($start)));
|
|
$end_month = strtotime(gmdate('Y-m-01', strtotime($end)));
|
|
while ($cursor <= $end_month) {
|
|
$month_key = gmdate('Y-m', $cursor);
|
|
$data[] = [
|
|
'label' => wp_date('M Y', $cursor),
|
|
'views' => $indexed[$month_key] ?? 0,
|
|
];
|
|
$cursor = strtotime('+1 month', $cursor);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function get_target_weekday_breakdown_between(array $target, string $start, string $end) {
|
|
$daily_data = $this->get_target_daily_chart_data_between($target, $start, $end, 'views');
|
|
$weekday_rows = [
|
|
__('Mon', 'ansico-stat-plugin') => 0,
|
|
__('Tue', 'ansico-stat-plugin') => 0,
|
|
__('Wed', 'ansico-stat-plugin') => 0,
|
|
__('Thu', 'ansico-stat-plugin') => 0,
|
|
__('Fri', 'ansico-stat-plugin') => 0,
|
|
__('Sat', 'ansico-stat-plugin') => 0,
|
|
__('Sun', 'ansico-stat-plugin') => 0,
|
|
];
|
|
$weekday_keys = array_keys($weekday_rows);
|
|
|
|
foreach ($daily_data as $row) {
|
|
$timestamp = strtotime((string) ($row['date'] ?? ''));
|
|
if (!$timestamp) {
|
|
continue;
|
|
}
|
|
$weekday_index = (int) gmdate('N', $timestamp) - 1;
|
|
if (isset($weekday_keys[$weekday_index])) {
|
|
$weekday_rows[$weekday_keys[$weekday_index]] += (int) ($row['views'] ?? 0);
|
|
}
|
|
}
|
|
|
|
$data = [];
|
|
foreach ($weekday_rows as $label => $views) {
|
|
$data[] = [
|
|
'label' => $label,
|
|
'views' => (int) $views,
|
|
];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function render_dual_chart_markup(array $views_data, array $unique_data, string $chart_id, string $aria_label = '') {
|
|
if ($aria_label === '') {
|
|
$aria_label = __('Views and unique visitors chart', 'ansico-stat-plugin');
|
|
}
|
|
|
|
$count = max(count($views_data), count($unique_data));
|
|
if ($count < 1) {
|
|
return '<p>' . esc_html__('No chart data available.', 'ansico-stat-plugin') . '</p>';
|
|
}
|
|
|
|
$values = [];
|
|
foreach ($views_data as $row) {
|
|
$values[] = (int) ($row['views'] ?? 0);
|
|
}
|
|
foreach ($unique_data as $row) {
|
|
$values[] = (int) ($row['views'] ?? 0);
|
|
}
|
|
$max = max(1, !empty($values) ? max($values) : 1);
|
|
$width = 920;
|
|
$height = 240;
|
|
$chart_top = 20;
|
|
$chart_bottom = 185;
|
|
$chart_left = 26;
|
|
$chart_right = 830;
|
|
$stepX = $count > 1 ? ($chart_right - $chart_left) / ($count - 1) : ($chart_right - $chart_left);
|
|
|
|
$build_points = static function(array $series, string $series_label) use ($chart_top, $chart_bottom, $chart_left, $stepX, $max) {
|
|
$points = [];
|
|
foreach (array_values($series) as $index => $row) {
|
|
$value = (int) ($row['views'] ?? 0);
|
|
$x = $chart_left + ($stepX * $index);
|
|
$y = $value > 0 ? $chart_top + (($chart_bottom - $chart_top) * (1 - ($value / $max))) : $chart_bottom;
|
|
$label = (string) ($row['label'] ?? $row['date'] ?? '');
|
|
$points[] = [
|
|
'x' => $x,
|
|
'y' => $y,
|
|
'label' => $label,
|
|
'value' => $value,
|
|
'series_label' => $series_label,
|
|
];
|
|
}
|
|
return $points;
|
|
};
|
|
|
|
$view_points = $build_points($views_data, __('Views', 'ansico-stat-plugin'));
|
|
$unique_points = $build_points($unique_data, __('Unique visitors', 'ansico-stat-plugin'));
|
|
$view_path = implode(' ', array_map(static function($point) {
|
|
return $point['x'] . ',' . $point['y'];
|
|
}, $view_points));
|
|
$unique_path = implode(' ', array_map(static function($point) {
|
|
return $point['x'] . ',' . $point['y'];
|
|
}, $unique_points));
|
|
|
|
ob_start();
|
|
?>
|
|
<div class="ansico-stat-chart-wrap ansico-stat-chart-wrap-enhanced" id="<?php echo esc_attr($chart_id); ?>-wrap">
|
|
<div class="ansico-stat-line-chart ansico-stat-line-chart-enhanced ansico-stat-line-chart-dual" id="<?php echo esc_attr($chart_id); ?>">
|
|
<svg viewBox="0 0 <?php echo esc_attr((string) $width); ?> <?php echo esc_attr((string) $height); ?>" width="100%" height="<?php echo esc_attr((string) $height); ?>" role="img" aria-label="<?php echo esc_attr($aria_label); ?>">
|
|
<line x1="<?php echo esc_attr((string) $chart_left); ?>" y1="<?php echo esc_attr((string) $chart_bottom); ?>" x2="<?php echo esc_attr((string) $chart_right); ?>" y2="<?php echo esc_attr((string) $chart_bottom); ?>" stroke="#cbd5e1" stroke-width="1.2"></line>
|
|
<?php for ($i = 0; $i <= 4; $i++) : $ratio = $i / 4; $y = $chart_top + (($chart_bottom - $chart_top) * $ratio); ?>
|
|
<line x1="<?php echo esc_attr((string) $chart_left); ?>" y1="<?php echo esc_attr((string) $y); ?>" x2="<?php echo esc_attr((string) $chart_right); ?>" y2="<?php echo esc_attr((string) $y); ?>" stroke="#e5e7eb" stroke-width="1"></line>
|
|
<?php endfor; ?>
|
|
<?php if ($view_path !== '') : ?>
|
|
<polyline fill="none" points="<?php echo esc_attr($view_path); ?>" stroke="#2271b1" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></polyline>
|
|
<?php endif; ?>
|
|
<?php if ($unique_path !== '') : ?>
|
|
<polyline fill="none" points="<?php echo esc_attr($unique_path); ?>" stroke="#8c8f94" stroke-width="3" stroke-dasharray="7 5" stroke-linecap="round" stroke-linejoin="round"></polyline>
|
|
<?php endif; ?>
|
|
|
|
<?php foreach ($view_points as $point) : ?>
|
|
<rect class="ansico-stat-chart-point-hit" x="<?php echo esc_attr((string) ($point['x'] - 18)); ?>" y="<?php echo esc_attr((string) ($point['y'] - 18)); ?>" width="36" height="36" rx="18" ry="18" fill="transparent" data-label="<?php echo esc_attr($point['series_label'] . ' — ' . $point['label']); ?>" data-value="<?php echo esc_attr(number_format_i18n($point['value'])); ?>"></rect>
|
|
<circle class="ansico-stat-chart-point ansico-stat-chart-point-views" cx="<?php echo esc_attr((string) $point['x']); ?>" cy="<?php echo esc_attr((string) $point['y']); ?>" r="4" fill="#ffffff" stroke="#2271b1" stroke-width="2.5"></circle>
|
|
<?php endforeach; ?>
|
|
|
|
<?php foreach ($unique_points as $point) : ?>
|
|
<rect class="ansico-stat-chart-point-hit" x="<?php echo esc_attr((string) ($point['x'] - 18)); ?>" y="<?php echo esc_attr((string) ($point['y'] - 18)); ?>" width="36" height="36" rx="18" ry="18" fill="transparent" data-label="<?php echo esc_attr($point['series_label'] . ' — ' . $point['label']); ?>" data-value="<?php echo esc_attr(number_format_i18n($point['value'])); ?>"></rect>
|
|
<circle class="ansico-stat-chart-point ansico-stat-chart-point-unique" cx="<?php echo esc_attr((string) $point['x']); ?>" cy="<?php echo esc_attr((string) $point['y']); ?>" r="4" fill="#ffffff" stroke="#8c8f94" stroke-width="2.5"></circle>
|
|
<?php endforeach; ?>
|
|
</svg>
|
|
</div>
|
|
<div class="ansico-stat-chart-tooltip" style="display:none; left:-9999px; top:-9999px; opacity:0;" aria-hidden="true"></div>
|
|
<div class="ansico-stat-dual-chart-legend"><span><span class="ansico-stat-dual-chart-swatch ansico-stat-dual-chart-swatch-views"></span><?php echo esc_html__('Views', 'ansico-stat-plugin'); ?></span><span><span class="ansico-stat-dual-chart-swatch ansico-stat-dual-chart-swatch-unique"></span><?php echo esc_html__('Unique visitors', 'ansico-stat-plugin'); ?></span></div>
|
|
</div>
|
|
<script>
|
|
(function() {
|
|
var root = document.getElementById(<?php echo wp_json_encode($chart_id); ?>);
|
|
var wrap = document.getElementById(<?php echo wp_json_encode($chart_id . '-wrap'); ?>);
|
|
if (!root || !wrap) return;
|
|
var tooltip = wrap.querySelector('.ansico-stat-chart-tooltip');
|
|
var points = root.querySelectorAll('.ansico-stat-chart-point-hit');
|
|
if (!tooltip || !points.length) return;
|
|
|
|
function resetTooltip() {
|
|
tooltip.style.display = 'none';
|
|
tooltip.style.opacity = '0';
|
|
tooltip.style.left = '-9999px';
|
|
tooltip.style.top = '-9999px';
|
|
}
|
|
|
|
function positionTooltipForPoint(point) {
|
|
var wrapRect = wrap.getBoundingClientRect();
|
|
var pointRect = point.getBoundingClientRect();
|
|
var tipRect = tooltip.getBoundingClientRect();
|
|
|
|
var pointCenterX = (pointRect.left - wrapRect.left) + (pointRect.width / 2);
|
|
var pointTopY = (pointRect.top - wrapRect.top);
|
|
|
|
var left = pointCenterX - (tipRect.width / 2);
|
|
var top = pointTopY - tipRect.height - 10;
|
|
|
|
if (left + tipRect.width > wrapRect.width - 8) left = wrapRect.width - tipRect.width - 8;
|
|
if (left < 8) left = 8;
|
|
if (top < 8) top = pointTopY + 18;
|
|
|
|
tooltip.style.left = left + 'px';
|
|
tooltip.style.top = top + 'px';
|
|
}
|
|
|
|
function showTooltip(event) {
|
|
var point = event.currentTarget;
|
|
tooltip.textContent = point.getAttribute('data-label') + ': ' + point.getAttribute('data-value');
|
|
tooltip.style.display = 'block';
|
|
tooltip.style.opacity = '1';
|
|
positionTooltipForPoint(point);
|
|
}
|
|
|
|
points.forEach(function(point) {
|
|
point.addEventListener('mouseenter', showTooltip);
|
|
point.addEventListener('mouseleave', resetTooltip);
|
|
});
|
|
|
|
wrap.addEventListener('mouseleave', resetTooltip);
|
|
resetTooltip();
|
|
})();
|
|
</script>
|
|
<?php
|
|
return (string) ob_get_clean();
|
|
}
|
|
|
|
protected function get_target_unique_daily_chart_data(array $target, int $days = 60) {
|
|
global $wpdb;
|
|
|
|
$days = max(1, $days);
|
|
$start = $this->date_days_ago($days - 1);
|
|
$end = current_time('Y-m-d');
|
|
|
|
if (($target['kind'] ?? '') === 'singular') {
|
|
$table = self::post_daily_table_name();
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT stat_date, unique_visitors FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
|
|
(int) $target['post_id'],
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
} else {
|
|
$table = self::page_daily_table_name();
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT stat_date, unique_visitors FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
|
|
(string) $target['page_key'],
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
}
|
|
|
|
$indexed = [];
|
|
foreach ((array) $rows as $row) {
|
|
$indexed[(string) $row['stat_date']] = (int) $row['unique_visitors'];
|
|
}
|
|
|
|
$data = [];
|
|
$cursor = strtotime($start);
|
|
$end_ts = strtotime($end);
|
|
while ($cursor <= $end_ts) {
|
|
$date = gmdate('Y-m-d', $cursor);
|
|
$data[] = [
|
|
'date' => $date,
|
|
'views' => $indexed[$date] ?? 0,
|
|
];
|
|
$cursor = strtotime('+1 day', $cursor);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function get_target_period_views(array $target, string $start, string $end) {
|
|
global $wpdb;
|
|
|
|
if (($target['kind'] ?? '') === 'singular') {
|
|
$table = self::post_daily_table_name();
|
|
$views = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT SUM(views) FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s",
|
|
(int) $target['post_id'],
|
|
$start,
|
|
$end
|
|
));
|
|
return (int) $views;
|
|
}
|
|
|
|
$table = self::page_daily_table_name();
|
|
$views = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT SUM(views) FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s",
|
|
(string) $target['page_key'],
|
|
$start,
|
|
$end
|
|
));
|
|
return (int) $views;
|
|
}
|
|
|
|
protected function get_target_daily_chart_data(array $target, int $days = 60) {
|
|
global $wpdb;
|
|
|
|
$days = max(1, $days);
|
|
$start = $this->date_days_ago($days - 1);
|
|
$end = current_time('Y-m-d');
|
|
|
|
if (($target['kind'] ?? '') === 'singular') {
|
|
$table = self::post_daily_table_name();
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT stat_date, views FROM {$table} WHERE post_id = %d AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
|
|
(int) $target['post_id'],
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
} else {
|
|
$table = self::page_daily_table_name();
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT stat_date, views FROM {$table} WHERE page_key = %s AND stat_date BETWEEN %s AND %s ORDER BY stat_date ASC",
|
|
(string) $target['page_key'],
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
}
|
|
|
|
$indexed = [];
|
|
foreach ((array) $rows as $row) {
|
|
$indexed[(string) $row['stat_date']] = (int) $row['views'];
|
|
}
|
|
|
|
$data = [];
|
|
$cursor = strtotime($start);
|
|
$end_ts = strtotime($end);
|
|
while ($cursor <= $end_ts) {
|
|
$date = gmdate('Y-m-d', $cursor);
|
|
$data[] = [
|
|
'date' => $date,
|
|
'views' => $indexed[$date] ?? 0,
|
|
];
|
|
$cursor = strtotime('+1 day', $cursor);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function get_target_monthly_chart_data(array $target, int $months = 12) {
|
|
global $wpdb;
|
|
|
|
$months = max(1, $months);
|
|
$current_month = strtotime(wp_date('Y-m-01', current_time('timestamp')));
|
|
$start_month = strtotime('-' . ($months - 1) . ' months', $current_month);
|
|
$start = wp_date('Y-m-01', $start_month);
|
|
$end = wp_date('Y-m-t', current_time('timestamp'));
|
|
|
|
if (($target['kind'] ?? '') === 'singular') {
|
|
$table = self::post_daily_table_name();
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT DATE_FORMAT(stat_date, '%%Y-%%m') AS month_key, SUM(views) AS total_views\n FROM {$table}\n WHERE post_id = %d AND stat_date BETWEEN %s AND %s\n GROUP BY month_key\n ORDER BY month_key ASC",
|
|
(int) $target['post_id'],
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
} else {
|
|
$table = self::page_daily_table_name();
|
|
$rows = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT DATE_FORMAT(stat_date, '%%Y-%%m') AS month_key, SUM(views) AS total_views\n FROM {$table}\n WHERE page_key = %s AND stat_date BETWEEN %s AND %s\n GROUP BY month_key\n ORDER BY month_key ASC",
|
|
(string) $target['page_key'],
|
|
$start,
|
|
$end
|
|
), ARRAY_A);
|
|
}
|
|
|
|
$indexed = [];
|
|
foreach ((array) $rows as $row) {
|
|
$indexed[(string) $row['month_key']] = (int) $row['total_views'];
|
|
}
|
|
|
|
$data = [];
|
|
$cursor = $start_month;
|
|
while ($cursor <= $current_month) {
|
|
$month_key = wp_date('Y-m', $cursor);
|
|
$data[] = [
|
|
'label' => wp_date('M Y', $cursor),
|
|
'views' => $indexed[$month_key] ?? 0,
|
|
];
|
|
$cursor = strtotime('+1 month', $cursor);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function get_target_best_day(array $target) {
|
|
global $wpdb;
|
|
|
|
if (($target['kind'] ?? '') === 'singular') {
|
|
$table = self::post_daily_table_name();
|
|
$row = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT stat_date, views FROM {$table} WHERE post_id = %d ORDER BY views DESC, stat_date ASC LIMIT 1",
|
|
(int) $target['post_id']
|
|
), ARRAY_A);
|
|
} else {
|
|
$table = self::page_daily_table_name();
|
|
$row = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT stat_date, views FROM {$table} WHERE page_key = %s ORDER BY views DESC, stat_date ASC LIMIT 1",
|
|
(string) $target['page_key']
|
|
), ARRAY_A);
|
|
}
|
|
|
|
return is_array($row) ? $row : [];
|
|
}
|
|
|
|
protected function get_target_first_tracked_date(array $target) {
|
|
global $wpdb;
|
|
|
|
if (($target['kind'] ?? '') === 'singular') {
|
|
$table = self::post_daily_table_name();
|
|
$date = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT MIN(stat_date) FROM {$table} WHERE post_id = %d",
|
|
(int) $target['post_id']
|
|
));
|
|
} else {
|
|
$table = self::page_daily_table_name();
|
|
$date = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT MIN(stat_date) FROM {$table} WHERE page_key = %s",
|
|
(string) $target['page_key']
|
|
));
|
|
}
|
|
|
|
return is_string($date) ? $date : '';
|
|
}
|
|
|
|
|
|
protected function get_target_total_tracked_days(array $target) {
|
|
global $wpdb;
|
|
|
|
if (($target['kind'] ?? '') === 'singular') {
|
|
$table = self::post_daily_table_name();
|
|
$count = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$table} WHERE post_id = %d",
|
|
(int) $target['post_id']
|
|
));
|
|
} else {
|
|
$table = self::page_daily_table_name();
|
|
$count = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$table} WHERE page_key = %s",
|
|
(string) $target['page_key']
|
|
));
|
|
}
|
|
|
|
return (int) $count;
|
|
}
|
|
|
|
|
|
protected function get_target_average_unique_visitors_per_day(array $target, int $days) {
|
|
$days = max(1, $days);
|
|
$uniques = $this->get_target_period_unique_visitors($target, $this->date_days_ago($days - 1), current_time('Y-m-d'));
|
|
return $uniques / $days;
|
|
}
|
|
|
|
protected function get_target_unique_period_comparison(array $target, int $days) {
|
|
$days = max(1, $days);
|
|
$current_end = current_time('Y-m-d');
|
|
$current_start = $this->date_days_ago($days - 1);
|
|
$previous_end = $this->date_days_ago($days);
|
|
$previous_start = $this->date_days_ago(($days * 2) - 1);
|
|
|
|
$current_uniques = $this->get_target_period_unique_visitors($target, $current_start, $current_end);
|
|
$previous_uniques = $this->get_target_period_unique_visitors($target, $previous_start, $previous_end);
|
|
$change = $current_uniques - $previous_uniques;
|
|
$percent_change = null;
|
|
if ($previous_uniques > 0) {
|
|
$percent_change = (($current_uniques - $previous_uniques) / $previous_uniques) * 100;
|
|
} elseif ($current_uniques > 0) {
|
|
$percent_change = 100.0;
|
|
}
|
|
|
|
return [
|
|
'days' => $days,
|
|
'current_views' => (int) $current_uniques,
|
|
'previous_views' => (int) $previous_uniques,
|
|
'change' => (int) $change,
|
|
'percent_change' => $percent_change,
|
|
'current_start' => $current_start,
|
|
'current_end' => $current_end,
|
|
'previous_start' => $previous_start,
|
|
'previous_end' => $previous_end,
|
|
];
|
|
}
|
|
|
|
protected function get_target_average_views_per_day(array $target, int $days) {
|
|
$days = max(1, $days);
|
|
$views = $this->get_target_period_views($target, $this->date_days_ago($days - 1), current_time('Y-m-d'));
|
|
return $views / $days;
|
|
}
|
|
|
|
protected function get_target_period_comparison(array $target, int $days) {
|
|
$days = max(1, $days);
|
|
$current_end = current_time('Y-m-d');
|
|
$current_start = $this->date_days_ago($days - 1);
|
|
$previous_end = $this->date_days_ago($days);
|
|
$previous_start = $this->date_days_ago(($days * 2) - 1);
|
|
|
|
$current_views = $this->get_target_period_views($target, $current_start, $current_end);
|
|
$previous_views = $this->get_target_period_views($target, $previous_start, $previous_end);
|
|
$change = $current_views - $previous_views;
|
|
$percent_change = null;
|
|
if ($previous_views > 0) {
|
|
$percent_change = (($current_views - $previous_views) / $previous_views) * 100;
|
|
} elseif ($current_views > 0) {
|
|
$percent_change = 100.0;
|
|
}
|
|
|
|
return [
|
|
'days' => $days,
|
|
'current_views' => (int) $current_views,
|
|
'previous_views' => (int) $previous_views,
|
|
'change' => (int) $change,
|
|
'percent_change' => $percent_change,
|
|
'current_start' => $current_start,
|
|
'current_end' => $current_end,
|
|
'previous_start' => $previous_start,
|
|
'previous_end' => $previous_end,
|
|
];
|
|
}
|
|
|
|
protected function format_change_percent($percent_change) {
|
|
if ($percent_change === null) {
|
|
return __('New data', 'ansico-stat-plugin');
|
|
}
|
|
|
|
$prefix = $percent_change > 0 ? '+' : '';
|
|
return $prefix . number_format_i18n((float) $percent_change, 1) . '%';
|
|
}
|
|
|
|
protected function get_target_weekday_breakdown(array $target, int $days = 90) {
|
|
$days = max(1, $days);
|
|
$daily_data = $this->get_target_daily_chart_data($target, $days);
|
|
$weekday_rows = [
|
|
__('Mon', 'ansico-stat-plugin') => 0,
|
|
__('Tue', 'ansico-stat-plugin') => 0,
|
|
__('Wed', 'ansico-stat-plugin') => 0,
|
|
__('Thu', 'ansico-stat-plugin') => 0,
|
|
__('Fri', 'ansico-stat-plugin') => 0,
|
|
__('Sat', 'ansico-stat-plugin') => 0,
|
|
__('Sun', 'ansico-stat-plugin') => 0,
|
|
];
|
|
$weekday_keys = array_keys($weekday_rows);
|
|
|
|
foreach ($daily_data as $row) {
|
|
$timestamp = strtotime((string) ($row['date'] ?? ''));
|
|
if (!$timestamp) {
|
|
continue;
|
|
}
|
|
$weekday_index = (int) gmdate('N', $timestamp) - 1;
|
|
if (!isset($weekday_keys[$weekday_index])) {
|
|
continue;
|
|
}
|
|
$weekday_rows[$weekday_keys[$weekday_index]] += (int) ($row['views'] ?? 0);
|
|
}
|
|
|
|
$data = [];
|
|
foreach ($weekday_rows as $label => $views) {
|
|
$data[] = [
|
|
'label' => $label,
|
|
'views' => (int) $views,
|
|
];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function build_single_export_url(string $target_url, string $start_date = '', string $end_date = '') {
|
|
$args = [
|
|
'action' => 'ansico_stat_export_single_csv',
|
|
'target_url' => $target_url,
|
|
];
|
|
if ($start_date !== '') {
|
|
$args['start_date'] = $start_date;
|
|
}
|
|
if ($end_date !== '') {
|
|
$args['end_date'] = $end_date;
|
|
}
|
|
|
|
return wp_nonce_url(
|
|
add_query_arg($args, admin_url('admin-post.php')),
|
|
'ansico_stat_export_single_csv'
|
|
);
|
|
}
|
|
|
|
protected function render_single_stats_summary_cards(array $items) {
|
|
echo '<div class="ansico-stat-summary-grid">';
|
|
foreach ($items as $item) {
|
|
echo '<div class="ansico-stat-summary-card">';
|
|
echo '<div class="ansico-stat-summary-label">' . esc_html((string) ($item['label'] ?? '')) . '</div>';
|
|
echo '<div class="ansico-stat-summary-value">' . esc_html((string) ($item['value'] ?? '')) . '</div>';
|
|
if (!empty($item['description'])) {
|
|
echo '<div class="ansico-stat-summary-description">' . esc_html((string) $item['description']) . '</div>';
|
|
}
|
|
echo '</div>';
|
|
}
|
|
echo '</div>';
|
|
}
|
|
|
|
public function render_single_page_stats_page() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(esc_html__('You do not have permission to access this page.', 'ansico-stat-plugin'));
|
|
}
|
|
|
|
$requested_url = $this->get_requested_single_stats_url();
|
|
$date_range = $this->get_single_stats_date_range();
|
|
$range_start = (string) $date_range['start'];
|
|
$range_end = (string) $date_range['end'];
|
|
$range_days = (int) $date_range['days'];
|
|
$result = $this->resolve_single_stats_target($requested_url);
|
|
$target = !empty($result['success']) ? (array) $result['target'] : [];
|
|
$daily_chart = !empty($target) ? $this->get_target_daily_chart_data_between($target, $range_start, $range_end, 'views') : [];
|
|
$unique_daily_chart = !empty($target) ? $this->get_target_daily_chart_data_between($target, $range_start, $range_end, 'unique_visitors') : [];
|
|
$monthly_chart = !empty($target) ? $this->get_target_monthly_chart_data_between($target, $range_start, $range_end) : [];
|
|
$weekday_chart = !empty($target) ? $this->get_target_weekday_breakdown_between($target, $range_start, $range_end) : [];
|
|
$lifetime_views = !empty($target) ? $this->get_target_lifetime_views($target) : 0;
|
|
$lifetime_unique_visitors = !empty($target) ? $this->get_target_lifetime_unique_visitors($target) : 0;
|
|
$views_today = !empty($target) ? $this->get_target_period_views($target, current_time('Y-m-d'), current_time('Y-m-d')) : 0;
|
|
$unique_today = !empty($target) ? $this->get_target_period_unique_visitors($target, current_time('Y-m-d'), current_time('Y-m-d')) : 0;
|
|
$range_views = !empty($target) ? $this->get_target_period_views($target, $range_start, $range_end) : 0;
|
|
$range_unique = !empty($target) ? $this->get_target_period_unique_visitors($target, $range_start, $range_end) : 0;
|
|
$best_day = !empty($target) ? $this->get_target_best_day($target) : [];
|
|
$first_tracked = !empty($target) ? $this->get_target_first_tracked_date($target) : '';
|
|
$tracked_days = !empty($target) ? $this->get_target_total_tracked_days($target) : 0;
|
|
$avg_range = $range_days > 0 ? ($range_views / $range_days) : 0;
|
|
$avg_unique_range = $range_days > 0 ? ($range_unique / $range_days) : 0;
|
|
$compare_range = !empty($target) ? $this->get_target_period_comparison($target, $range_days) : [];
|
|
$unique_compare_range = !empty($target) ? $this->get_target_unique_period_comparison($target, $range_days) : [];
|
|
$views_7 = !empty($target) ? $this->get_target_period_views($target, $this->date_days_ago(6), current_time('Y-m-d')) : 0;
|
|
$views_30 = !empty($target) ? $this->get_target_period_views($target, $this->date_days_ago(29), current_time('Y-m-d')) : 0;
|
|
$unique_7 = !empty($target) ? $this->get_target_period_unique_visitors($target, $this->date_days_ago(6), current_time('Y-m-d')) : 0;
|
|
$unique_30 = !empty($target) ? $this->get_target_period_unique_visitors($target, $this->date_days_ago(29), current_time('Y-m-d')) : 0;
|
|
$export_url = !empty($target) ? $this->build_single_export_url($requested_url, $range_start, $range_end) : '';
|
|
$reset_range_url = $this->build_single_stats_admin_url($requested_url);
|
|
$post_type_label = '';
|
|
if (!empty($target) && ($target['kind'] ?? '') === 'singular') {
|
|
$post_type_obj = get_post_type_object((string) ($target['post_type'] ?? ''));
|
|
$post_type_label = $post_type_obj && !empty($post_type_obj->labels->singular_name) ? (string) $post_type_obj->labels->singular_name : (string) ($target['post_type'] ?? '');
|
|
}
|
|
?>
|
|
<div class="wrap ansico-stat-admin-page">
|
|
<h1><?php echo esc_html__('Ansico Stat Plugin — Single page statistics', 'ansico-stat-plugin'); ?></h1>
|
|
<p><?php echo esc_html__('Enter a URL from this WordPress site to see the statistics already collected for that specific page, post, or tracked custom post type entry.', 'ansico-stat-plugin'); ?></p>
|
|
|
|
<div class="ansico-stat-card">
|
|
<form method="get" class="ansico-stat-url-form">
|
|
<input type="hidden" name="page" value="<?php echo esc_attr(self::MENU_SLUG_SINGLE); ?>" />
|
|
<div class="ansico-stat-url-form-row">
|
|
<div class="ansico-stat-url-field">
|
|
<label for="ansico-stat-target-url"><strong><?php echo esc_html__('URL', 'ansico-stat-plugin'); ?></strong></label>
|
|
<input type="url" class="regular-text code" style="width:100%;max-width:none;" id="ansico-stat-target-url" name="target_url" value="<?php echo esc_attr($requested_url); ?>" placeholder="<?php echo esc_attr(home_url('/')); ?>" />
|
|
</div>
|
|
<div class="ansico-stat-date-field">
|
|
<label for="ansico-stat-start-date"><strong><?php echo esc_html__('From', 'ansico-stat-plugin'); ?></strong></label>
|
|
<input type="date" id="ansico-stat-start-date" name="start_date" value="<?php echo esc_attr($range_start); ?>" />
|
|
</div>
|
|
<div class="ansico-stat-date-field">
|
|
<label for="ansico-stat-end-date"><strong><?php echo esc_html__('To', 'ansico-stat-plugin'); ?></strong></label>
|
|
<input type="date" id="ansico-stat-end-date" name="end_date" value="<?php echo esc_attr($range_end); ?>" />
|
|
</div>
|
|
<div class="ansico-stat-url-action">
|
|
<?php submit_button(__('Update', 'ansico-stat-plugin'), 'secondary', '', false); ?>
|
|
</div>
|
|
</div>
|
|
<p class="description" style="margin:10px 0 0 0;"><?php echo esc_html__('Leave the date fields as-is for the default 30-day report, or choose a custom range for the selected page.', 'ansico-stat-plugin'); ?> <?php if (!empty($date_range['is_custom'])) : ?><a href="<?php echo esc_url($reset_range_url); ?>"><?php echo esc_html__('Reset range', 'ansico-stat-plugin'); ?></a><?php endif; ?></p>
|
|
</form>
|
|
</div>
|
|
|
|
<?php if (empty($result['success'])) : ?>
|
|
<div class="ansico-stat-card">
|
|
<p><?php echo esc_html((string) ($result['message'] ?? __('No page selected.', 'ansico-stat-plugin'))); ?></p>
|
|
</div>
|
|
<?php else : ?>
|
|
<div class="ansico-stat-card">
|
|
<div class="ansico-stat-card-header">
|
|
<div>
|
|
<h2 style="margin-bottom:6px;"><?php echo esc_html((string) ($target['label'] ?? '')); ?></h2>
|
|
<p class="description" style="margin:0;"><?php echo esc_html((string) ($target['url'] ?? '')); ?></p>
|
|
<p class="description" style="margin:4px 0 0 0;"><?php echo esc_html(sprintf(__('Selected period: %1$s to %2$s (%3$d days)', 'ansico-stat-plugin'), wp_date(get_option('date_format'), strtotime($range_start)), wp_date(get_option('date_format'), strtotime($range_end)), $range_days)); ?></p>
|
|
</div>
|
|
<div class="ansico-stat-target-meta" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
|
<span class="ansico-stat-target-badge"><?php echo esc_html(($target['kind'] ?? '') === 'singular' ? ($post_type_label ?: __('Post', 'ansico-stat-plugin')) : (string) ($target['page_type'] ?? __('Page', 'ansico-stat-plugin'))); ?></span>
|
|
<a class="button button-secondary" href="<?php echo esc_url($export_url); ?>"><?php echo esc_html__('Export page CSV', 'ansico-stat-plugin'); ?></a>
|
|
</div>
|
|
</div>
|
|
<?php $this->render_single_stats_summary_cards([
|
|
[
|
|
'label' => __('Views in selected period', 'ansico-stat-plugin'),
|
|
'value' => number_format_i18n($range_views),
|
|
'description' => sprintf(__('Avg. %s per day', 'ansico-stat-plugin'), number_format_i18n($avg_range, 1)),
|
|
],
|
|
[
|
|
'label' => __('Unique visitors in selected period', 'ansico-stat-plugin'),
|
|
'value' => number_format_i18n($range_unique),
|
|
'description' => sprintf(__('Avg. %s per day', 'ansico-stat-plugin'), number_format_i18n($avg_unique_range, 1)),
|
|
],
|
|
[
|
|
'label' => __('Views today', 'ansico-stat-plugin'),
|
|
'value' => number_format_i18n($views_today),
|
|
'description' => __('Today only', 'ansico-stat-plugin'),
|
|
],
|
|
[
|
|
'label' => __('Unique visitors today', 'ansico-stat-plugin'),
|
|
'value' => number_format_i18n($unique_today),
|
|
'description' => __('Today only', 'ansico-stat-plugin'),
|
|
],
|
|
[
|
|
'label' => __('Views last 7 days', 'ansico-stat-plugin'),
|
|
'value' => number_format_i18n($views_7),
|
|
'description' => __('Rolling 7-day total', 'ansico-stat-plugin'),
|
|
],
|
|
[
|
|
'label' => __('Unique visitors last 7 days', 'ansico-stat-plugin'),
|
|
'value' => number_format_i18n($unique_7),
|
|
'description' => __('Rolling 7-day total', 'ansico-stat-plugin'),
|
|
],
|
|
[
|
|
'label' => __('Views last 30 days', 'ansico-stat-plugin'),
|
|
'value' => number_format_i18n($views_30),
|
|
'description' => __('Rolling 30-day total', 'ansico-stat-plugin'),
|
|
],
|
|
[
|
|
'label' => __('Unique visitors last 30 days', 'ansico-stat-plugin'),
|
|
'value' => number_format_i18n($unique_30),
|
|
'description' => __('Rolling 30-day total', 'ansico-stat-plugin'),
|
|
],
|
|
[
|
|
'label' => __('Lifetime views', 'ansico-stat-plugin'),
|
|
'value' => number_format_i18n($lifetime_views),
|
|
'description' => $first_tracked !== '' ? sprintf(__('Tracked since %s', 'ansico-stat-plugin'), wp_date(get_option('date_format'), strtotime($first_tracked))) : '',
|
|
],
|
|
[
|
|
'label' => __('Lifetime unique visitors', 'ansico-stat-plugin'),
|
|
'value' => number_format_i18n($lifetime_unique_visitors),
|
|
'description' => __('Stored from version 1.0.0.6 and forward', 'ansico-stat-plugin'),
|
|
],
|
|
[
|
|
'label' => __('Best day', 'ansico-stat-plugin'),
|
|
'value' => !empty($best_day['views']) ? number_format_i18n((int) $best_day['views']) : '0',
|
|
'description' => !empty($best_day['stat_date']) ? wp_date(get_option('date_format'), strtotime((string) $best_day['stat_date'])) : __('No tracked data yet', 'ansico-stat-plugin'),
|
|
],
|
|
[
|
|
'label' => __('Tracked days with views', 'ansico-stat-plugin'),
|
|
'value' => number_format_i18n($tracked_days),
|
|
'description' => __('Days stored in the daily statistics table', 'ansico-stat-plugin'),
|
|
],
|
|
]); ?>
|
|
<p class="description" style="margin-top:12px;"><?php echo esc_html__('Unique visitors are stored per page/per day from version 1.0.0.6 and forward. Older historical rows may still show 0 unique visitors because that data was not collected before this build.', 'ansico-stat-plugin'); ?></p>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html__('Views and unique visitors', 'ansico-stat-plugin'); ?></h2>
|
|
<?php echo $this->render_dual_chart_markup($daily_chart, $unique_daily_chart, 'ansico-single-target-dual-chart', __('Views and unique visitors for selected page', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html(sprintf(__('Daily views (%d days)', 'ansico-stat-plugin'), $range_days)); ?></h2>
|
|
<?php echo $this->render_chart_markup($daily_chart, 'ansico-single-target-daily-chart'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html(sprintf(__('Daily unique visitors (%d days)', 'ansico-stat-plugin'), $range_days)); ?></h2>
|
|
<?php echo $this->render_chart_markup($unique_daily_chart, 'ansico-single-target-unique-daily-chart'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html__('Monthly totals for selected period', 'ansico-stat-plugin'); ?></h2>
|
|
<?php echo $this->render_bar_chart_markup($monthly_chart, 'ansico-single-target-monthly-chart', __('Monthly totals for selected page', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html__('Weekday distribution for selected period', 'ansico-stat-plugin'); ?></h2>
|
|
<?php echo $this->render_bar_chart_markup($weekday_chart, 'ansico-single-target-weekday-chart', __('Views by weekday for selected page', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html__('Selected period comparison', 'ansico-stat-plugin'); ?></h2>
|
|
<table class="widefat striped ansico-stat-table">
|
|
<thead>
|
|
<tr>
|
|
<th><?php echo esc_html__('Metric', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('Current period', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('Previous period', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('Change', 'ansico-stat-plugin'); ?></th>
|
|
<th><?php echo esc_html__('Change %', 'ansico-stat-plugin'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><?php echo esc_html__('Views', 'ansico-stat-plugin'); ?></td>
|
|
<td><?php echo esc_html(number_format_i18n((int) ($compare_range['current_views'] ?? 0))); ?></td>
|
|
<td><?php echo esc_html(number_format_i18n((int) ($compare_range['previous_views'] ?? 0))); ?></td>
|
|
<td><?php echo esc_html(number_format_i18n((int) ($compare_range['change'] ?? 0))); ?></td>
|
|
<td><?php echo esc_html($this->format_change_percent($compare_range['percent_change'] ?? null)); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<td><?php echo esc_html__('Unique visitors', 'ansico-stat-plugin'); ?></td>
|
|
<td><?php echo esc_html(number_format_i18n((int) ($unique_compare_range['current_views'] ?? 0))); ?></td>
|
|
<td><?php echo esc_html(number_format_i18n((int) ($unique_compare_range['previous_views'] ?? 0))); ?></td>
|
|
<td><?php echo esc_html(number_format_i18n((int) ($unique_compare_range['change'] ?? 0))); ?></td>
|
|
<td><?php echo esc_html($this->format_change_percent($unique_compare_range['percent_change'] ?? null)); ?></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
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();
|
|
?>
|
|
<div class="wrap ansico-stat-admin-page">
|
|
<h1><?php echo esc_html__('Ansico Stat Plugin', 'ansico-stat-plugin'); ?></h1>
|
|
<p><?php echo esc_html__('Use this page to manage plugin settings and reset all collected view statistics.', 'ansico-stat-plugin'); ?></p>
|
|
<p class="description"><?php echo esc_html(sprintf(__('A single browser only counts one view per page within %d minutes. You can change this below.', 'ansico-stat-plugin'), (int) $settings['revisit_minutes'])); ?></p>
|
|
|
|
<?php if (isset($_GET['settings-updated']) && $_GET['settings-updated'] === 'true') : ?>
|
|
<div class="notice notice-success is-dismissible"><p><?php echo esc_html__('Settings saved.', 'ansico-stat-plugin'); ?></p></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (isset($_GET['reset']) && $_GET['reset'] === 'success') : ?>
|
|
<div class="notice notice-success is-dismissible"><p><?php echo esc_html__('All tracked view data has been reset.', 'ansico-stat-plugin'); ?></p></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html__('Tracking settings', 'ansico-stat-plugin'); ?></h2>
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
|
<?php wp_nonce_field('ansico_stat_save_settings'); ?>
|
|
<input type="hidden" name="action" value="ansico_stat_save_settings" />
|
|
<table class="form-table" role="presentation">
|
|
<tbody>
|
|
<tr>
|
|
<th scope="row"><?php echo esc_html__('Who should be counted', 'ansico-stat-plugin'); ?></th>
|
|
<td>
|
|
<fieldset>
|
|
<label><input type="radio" name="visitor_counting_mode" value="count_all" <?php checked($settings['visitor_counting_mode'], 'count_all'); ?> /> <?php echo esc_html__('Count everyone, including logged in users and administrators', 'ansico-stat-plugin'); ?></label><br />
|
|
<label><input type="radio" name="visitor_counting_mode" value="exclude_admins" <?php checked($settings['visitor_counting_mode'], 'exclude_admins'); ?> /> <?php echo esc_html__('Count logged in users, but exclude administrators', 'ansico-stat-plugin'); ?></label><br />
|
|
<label><input type="radio" name="visitor_counting_mode" value="exclude_all_logged_in" <?php checked($settings['visitor_counting_mode'], 'exclude_all_logged_in'); ?> /> <?php echo esc_html__('Exclude all logged in users', 'ansico-stat-plugin'); ?></label>
|
|
</fieldset>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo esc_html__('Count the same visitor again after', 'ansico-stat-plugin'); ?></th>
|
|
<td>
|
|
<input type="number" name="revisit_minutes" min="1" max="10080" value="<?php echo esc_attr((int) $settings['revisit_minutes']); ?>" class="small-text" />
|
|
<span><?php echo esc_html__('minutes', 'ansico-stat-plugin'); ?></span>
|
|
<p class="description"><?php echo esc_html__('This controls how long the plugin waits before the same browser can count as another view for the same page again.', 'ansico-stat-plugin'); ?></p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo esc_html__('Exclude known bots and crawlers', 'ansico-stat-plugin'); ?></th>
|
|
<td>
|
|
<label>
|
|
<input type="checkbox" name="exclude_known_bots" value="1" <?php checked(!empty($settings['exclude_known_bots'])); ?> />
|
|
<?php echo esc_html__('Skip tracking for requests that look like bots, crawlers, monitoring tools, or previews based on the user agent and request headers.', 'ansico-stat-plugin'); ?>
|
|
</label>
|
|
<p class="description"><?php echo esc_html__('Bot detection is heuristic. It blocks many common bots, but no detection method is perfect.', 'ansico-stat-plugin'); ?></p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo esc_html__('Footer label', 'ansico-stat-plugin'); ?></th>
|
|
<td>
|
|
<input type="text" name="frontend_label" value="<?php echo esc_attr((string) $settings['frontend_label']); ?>" class="regular-text" />
|
|
<p class="description"><?php echo esc_html__('This text is shown before the count in the admin-only footer display. Default: Views', 'ansico-stat-plugin'); ?></p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo esc_html__('Track singular content types', 'ansico-stat-plugin'); ?></th>
|
|
<td>
|
|
<?php
|
|
$public_post_types = get_post_types(['public' => true], 'objects');
|
|
$excluded_types = ['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'];
|
|
foreach ($public_post_types as $post_type => $post_type_object) :
|
|
if (in_array($post_type, $excluded_types, true)) {
|
|
continue;
|
|
}
|
|
$label = $post_type_object->labels->singular_name ?? $post_type;
|
|
?>
|
|
<label style="display:block; margin-bottom:6px;">
|
|
<input type="checkbox" name="track_post_types[]" value="<?php echo esc_attr($post_type); ?>" <?php checked(in_array($post_type, $settings['track_post_types'], true)); ?> />
|
|
<?php echo esc_html($label . ' (' . $post_type . ')'); ?>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
<p class="description"><?php echo esc_html__('Choose which posts, pages, and public custom post types should be tracked.', 'ansico-stat-plugin'); ?></p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo esc_html__('Track other page types', 'ansico-stat-plugin'); ?></th>
|
|
<td>
|
|
<label style="display:block; margin-bottom:6px;"><input type="checkbox" name="track_front_page" value="1" <?php checked(!empty($settings['track_front_page'])); ?> /> <?php echo esc_html__('Front page', 'ansico-stat-plugin'); ?></label>
|
|
<label style="display:block; margin-bottom:6px;"><input type="checkbox" name="track_posts_page" value="1" <?php checked(!empty($settings['track_posts_page'])); ?> /> <?php echo esc_html__('Posts page / blog home', 'ansico-stat-plugin'); ?></label>
|
|
<label style="display:block; margin-bottom:6px;"><input type="checkbox" name="track_archives" value="1" <?php checked(!empty($settings['track_archives'])); ?> /> <?php echo esc_html__('Archive pages, including categories, tags, taxonomies, authors, date archives, and post type archives', 'ansico-stat-plugin'); ?></label>
|
|
<label style="display:block; margin-bottom:6px;"><input type="checkbox" name="track_search" value="1" <?php checked(!empty($settings['track_search'])); ?> /> <?php echo esc_html__('Search results', 'ansico-stat-plugin'); ?></label>
|
|
<label style="display:block; margin-bottom:6px;"><input type="checkbox" name="track_404" value="1" <?php checked(!empty($settings['track_404'])); ?> /> <?php echo esc_html__('404 pages', 'ansico-stat-plugin'); ?></label>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo esc_html__('Rows in top lists', 'ansico-stat-plugin'); ?></th>
|
|
<td>
|
|
<input type="number" name="top_list_rows" min="1" max="100" value="<?php echo esc_attr((int) $settings['top_list_rows']); ?>" class="small-text" />
|
|
<p class="description"><?php echo esc_html__('Controls how many rows are shown in top lists such as monthly, yearly, dashboard, and 404 lists.', 'ansico-stat-plugin'); ?></p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row"><?php echo esc_html__('Rows in referral tables', 'ansico-stat-plugin'); ?></th>
|
|
<td>
|
|
<input type="number" name="referral_rows" min="1" max="200" value="<?php echo esc_attr((int) $settings['referral_rows']); ?>" class="small-text" />
|
|
<p class="description"><?php echo esc_html__('Controls how many referral URLs are shown in each referral table.', 'ansico-stat-plugin'); ?></p>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<?php submit_button(__('Save settings', 'ansico-stat-plugin')); ?>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html__('Reset all post views', 'ansico-stat-plugin'); ?></h2>
|
|
<p><?php echo esc_html__('This deletes all lifetime counters and all daily, monthly, yearly, and referral statistics for posts, pages, and public custom post types. This action cannot be undone.', 'ansico-stat-plugin'); ?></p>
|
|
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" onsubmit="return confirm('<?php echo esc_js(__('Are you sure you want to reset all Ansico Stat data?', 'ansico-stat-plugin')); ?>');">
|
|
<?php wp_nonce_field('ansico_stat_reset_all'); ?>
|
|
<input type="hidden" name="action" value="ansico_stat_reset_all" />
|
|
<?php submit_button(__('Reset all post views and statistics', 'ansico-stat-plugin'), 'delete'); ?>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
|
|
public function render_admin_page() {
|
|
$this->render_monthly_stats_page();
|
|
}
|
|
|
|
public function render_monthly_stats_page() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(esc_html__('You do not have permission to access this page.', 'ansico-stat-plugin'));
|
|
}
|
|
|
|
$settings = $this->get_settings();
|
|
$top_rows = max(1, min(100, (int) $settings['top_list_rows']));
|
|
$referral_rows_limit = max(1, min(200, (int) $settings['referral_rows']));
|
|
$chart_data = $this->get_daily_chart_data(60);
|
|
$selected_month = $this->get_selected_month();
|
|
$monthly_rows = $this->get_monthly_top_posts($selected_month, $top_rows);
|
|
$referral_summary = $this->get_referral_summary_for_month($selected_month);
|
|
$pie_summary = [
|
|
__('Direct', 'ansico-stat-plugin') => (int) $referral_summary['direct'],
|
|
__('Search engines', 'ansico-stat-plugin') => (int) $referral_summary['search'],
|
|
__('Social media', 'ansico-stat-plugin') => (int) $referral_summary['social'],
|
|
__('Other websites', 'ansico-stat-plugin') => (int) $referral_summary['website'],
|
|
];
|
|
$search_referrals = $this->get_referral_sources_for_month($selected_month, 'search', $referral_rows_limit);
|
|
$social_referrals = $this->get_referral_sources_for_month($selected_month, 'social', $referral_rows_limit);
|
|
$website_referrals = $this->get_referral_sources_for_month($selected_month, 'website', $referral_rows_limit);
|
|
$monthly_404_rows = $this->get_top_404_pages_for_month($selected_month, $top_rows);
|
|
$monthly_country_rows = $this->get_dimension_rows_for_month($selected_month, 'country', $top_rows);
|
|
$monthly_device_rows = $this->get_dimension_rows_for_month($selected_month, 'device', max(3, $top_rows));
|
|
$monthly_browser_rows = $this->get_dimension_rows_for_month($selected_month, 'browser', $top_rows);
|
|
$monthly_os_rows = $this->get_dimension_rows_for_month($selected_month, 'os', $top_rows);
|
|
?>
|
|
<div class="wrap ansico-stat-admin-page">
|
|
<h1><?php echo esc_html__('Ansico Stat Plugin — Monthly statistics', 'ansico-stat-plugin'); ?></h1>
|
|
<p><?php echo esc_html__('This page shows monthly rankings, referral source breakdowns, and visitor device details.', 'ansico-stat-plugin'); ?></p>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html__('Daily total views (last 60 days)', 'ansico-stat-plugin'); ?></h2>
|
|
<?php echo $this->render_chart_markup($chart_data, 'ansico-stat-admin-chart'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<div class="ansico-stat-card-header">
|
|
<h2><?php echo esc_html(sprintf(__('Monthly top %d views', 'ansico-stat-plugin'), (int) $top_rows)); ?></h2>
|
|
<a class="button button-secondary" href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=ansico_stat_export_csv'), 'ansico_stat_export_csv')); ?>"><?php echo esc_html__('Export CSV', 'ansico-stat-plugin'); ?></a>
|
|
</div>
|
|
<?php $this->render_month_navigation($selected_month); ?>
|
|
<?php $this->render_top_posts_table($monthly_rows, __('Monthly views', 'ansico-stat-plugin')); ?>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html(sprintf(__('Monthly top %d 404 visitors', 'ansico-stat-plugin'), (int) $top_rows)); ?></h2>
|
|
<?php $this->render_month_navigation($selected_month); ?>
|
|
<?php $this->render_top_404_table($monthly_404_rows); ?>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html__('Monthly referred visitors', 'ansico-stat-plugin'); ?></h2>
|
|
<?php $this->render_month_navigation($selected_month); ?>
|
|
<div class="ansico-stat-two-column">
|
|
<div>
|
|
<?php $this->render_referral_summary_table($referral_summary); ?>
|
|
</div>
|
|
<div>
|
|
<?php echo $this->render_pie_chart_markup($pie_summary, 'ansico-monthly-referral-pie', __('Monthly referred visitors', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top:1.5rem; display:grid; gap:1.5rem;">
|
|
<div><?php $this->render_referral_sources_table($search_referrals, __('Search engine referral URLs', 'ansico-stat-plugin')); ?></div>
|
|
<div><?php $this->render_referral_sources_table($social_referrals, __('Social media referral URLs', 'ansico-stat-plugin')); ?></div>
|
|
<div><?php $this->render_referral_sources_table($website_referrals, __('Other website referral URLs', 'ansico-stat-plugin')); ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!$this->is_unknown_only_dimension_rows($monthly_country_rows)) : ?>
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html(sprintf(__('Monthly countries (top %d)', 'ansico-stat-plugin'), (int) $top_rows)); ?></h2>
|
|
<?php $this->render_month_navigation($selected_month); ?>
|
|
<p class="description"><?php echo esc_html__('Country detection depends on country headers provided by your server or CDN, such as Cloudflare. This plugin does not perform IP geolocation by itself.', 'ansico-stat-plugin'); ?></p>
|
|
<div class="ansico-stat-two-column">
|
|
<div><?php $this->render_dimension_table($monthly_country_rows, __('Country', 'ansico-stat-plugin')); ?></div>
|
|
<div><?php echo $this->render_bar_chart_markup($monthly_country_rows, 'ansico-monthly-country-chart', __('Monthly countries', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html__('Monthly device types', 'ansico-stat-plugin'); ?></h2>
|
|
<?php $this->render_month_navigation($selected_month); ?>
|
|
<div class="ansico-stat-two-column">
|
|
<div><?php $this->render_dimension_table($monthly_device_rows, __('Device type', 'ansico-stat-plugin')); ?></div>
|
|
<div><?php echo $this->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 ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html(sprintf(__('Monthly browsers (top %d)', 'ansico-stat-plugin'), (int) $top_rows)); ?></h2>
|
|
<?php $this->render_month_navigation($selected_month); ?>
|
|
<div class="ansico-stat-two-column">
|
|
<div><?php $this->render_dimension_table($monthly_browser_rows, __('Browser', 'ansico-stat-plugin')); ?></div>
|
|
<div><?php echo $this->render_bar_chart_markup($monthly_browser_rows, 'ansico-monthly-browser-chart', __('Monthly browsers', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html(sprintf(__('Monthly operating systems (top %d)', 'ansico-stat-plugin'), (int) $top_rows)); ?></h2>
|
|
<?php $this->render_month_navigation($selected_month); ?>
|
|
<div class="ansico-stat-two-column">
|
|
<div><?php $this->render_dimension_table($monthly_os_rows, __('Operating system', 'ansico-stat-plugin')); ?></div>
|
|
<div><?php echo $this->render_bar_chart_markup($monthly_os_rows, 'ansico-monthly-os-chart', __('Monthly operating systems', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
public function render_yearly_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']));
|
|
$selected_year = $this->get_selected_year();
|
|
$yearly_rows = $this->get_yearly_top_posts($selected_year, $top_rows);
|
|
$yearly_country_rows = $this->get_dimension_rows_for_year($selected_year, 'country', $top_rows);
|
|
$yearly_device_rows = $this->get_dimension_rows_for_year($selected_year, 'device', max(3, $top_rows));
|
|
$yearly_browser_rows = $this->get_dimension_rows_for_year($selected_year, 'browser', $top_rows);
|
|
$yearly_os_rows = $this->get_dimension_rows_for_year($selected_year, 'os', $top_rows);
|
|
$yearly_referral_summary = $this->get_referral_summary_for_year($selected_year);
|
|
$yearly_month_chart = $this->get_monthly_totals_for_year($selected_year);
|
|
?>
|
|
<div class="wrap ansico-stat-admin-page">
|
|
<h1><?php echo esc_html__('Ansico Stat Plugin — Yearly statistics', 'ansico-stat-plugin'); ?></h1>
|
|
<p><?php echo esc_html__('This page shows yearly rankings and yearly visitor technology and country breakdowns.', 'ansico-stat-plugin'); ?></p>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html__('Yearly view totals by month', 'ansico-stat-plugin'); ?></h2>
|
|
<?php $this->render_year_navigation($selected_year); ?>
|
|
<?php echo $this->render_bar_chart_markup($yearly_month_chart, 'ansico-yearly-month-bar', __('Yearly views by month', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html(sprintf(__('Yearly top %d views', 'ansico-stat-plugin'), (int) $top_rows)); ?></h2>
|
|
<?php $this->render_year_navigation($selected_year); ?>
|
|
<?php $this->render_top_posts_table($yearly_rows, __('Yearly views', 'ansico-stat-plugin')); ?>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html__('Yearly referred visitors', 'ansico-stat-plugin'); ?></h2>
|
|
<?php $this->render_year_navigation($selected_year); ?>
|
|
<div class="ansico-stat-two-column">
|
|
<div>
|
|
<?php $this->render_referral_summary_table([
|
|
'direct' => (int) ($yearly_referral_summary['Direct'] ?? 0),
|
|
'search' => (int) ($yearly_referral_summary['Search engines'] ?? 0),
|
|
'social' => (int) ($yearly_referral_summary['Social media'] ?? 0),
|
|
'website' => (int) ($yearly_referral_summary['Other websites'] ?? 0),
|
|
]); ?>
|
|
</div>
|
|
<div><?php echo $this->render_pie_chart_markup($yearly_referral_summary, 'ansico-yearly-referral-pie', __('Yearly referred visitors', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!$this->is_unknown_only_dimension_rows($yearly_country_rows)) : ?>
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html(sprintf(__('Yearly countries (top %d)', 'ansico-stat-plugin'), (int) $top_rows)); ?></h2>
|
|
<?php $this->render_year_navigation($selected_year); ?>
|
|
<div class="ansico-stat-two-column">
|
|
<div><?php $this->render_dimension_table($yearly_country_rows, __('Country', 'ansico-stat-plugin')); ?></div>
|
|
<div><?php echo $this->render_bar_chart_markup($yearly_country_rows, 'ansico-yearly-country-chart', __('Yearly countries', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html__('Yearly device types', 'ansico-stat-plugin'); ?></h2>
|
|
<?php $this->render_year_navigation($selected_year); ?>
|
|
<div class="ansico-stat-two-column">
|
|
<div><?php $this->render_dimension_table($yearly_device_rows, __('Device type', 'ansico-stat-plugin')); ?></div>
|
|
<div><?php echo $this->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 ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html(sprintf(__('Yearly browsers (top %d)', 'ansico-stat-plugin'), (int) $top_rows)); ?></h2>
|
|
<?php $this->render_year_navigation($selected_year); ?>
|
|
<div class="ansico-stat-two-column">
|
|
<div><?php $this->render_dimension_table($yearly_browser_rows, __('Browser', 'ansico-stat-plugin')); ?></div>
|
|
<div><?php echo $this->render_bar_chart_markup($yearly_browser_rows, 'ansico-yearly-browser-chart', __('Yearly browsers', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ansico-stat-card">
|
|
<h2><?php echo esc_html(sprintf(__('Yearly operating systems (top %d)', 'ansico-stat-plugin'), (int) $top_rows)); ?></h2>
|
|
<?php $this->render_year_navigation($selected_year); ?>
|
|
<div class="ansico-stat-two-column">
|
|
<div><?php $this->render_dimension_table($yearly_os_rows, __('Operating system', 'ansico-stat-plugin')); ?></div>
|
|
<div><?php echo $this->render_bar_chart_markup($yearly_os_rows, 'ansico-yearly-os-chart', __('Yearly operating systems', 'ansico-stat-plugin')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
|
|
public function handle_export_single_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_single_csv');
|
|
|
|
$requested_url = isset($_GET['target_url']) ? sanitize_text_field(wp_unslash($_GET['target_url'])) : '';
|
|
$result = $this->resolve_single_stats_target($requested_url);
|
|
if (empty($result['success']) || empty($result['target'])) {
|
|
wp_die(esc_html__('No tracked page could be resolved for export.', 'ansico-stat-plugin'));
|
|
}
|
|
|
|
$target = (array) $result['target'];
|
|
$start_date = isset($_GET['start_date']) ? $this->sanitize_stats_date((string) wp_unslash($_GET['start_date'])) : '';
|
|
$end_date = isset($_GET['end_date']) ? $this->sanitize_stats_date((string) wp_unslash($_GET['end_date'])) : '';
|
|
if ($start_date === '') {
|
|
$start_date = $this->date_days_ago(29);
|
|
}
|
|
if ($end_date === '') {
|
|
$end_date = current_time('Y-m-d');
|
|
}
|
|
if (strtotime($start_date) > strtotime($end_date)) {
|
|
[$start_date, $end_date] = [$end_date, $start_date];
|
|
}
|
|
$daily_rows = $this->get_target_daily_chart_data_between($target, $start_date, $end_date, 'views');
|
|
$range_days = max(1, (int) floor((strtotime($end_date) - strtotime($start_date)) / DAY_IN_SECONDS) + 1);
|
|
$compare_7 = $this->get_target_period_comparison($target, 7);
|
|
$compare_30 = $this->get_target_period_comparison($target, 30);
|
|
$compare_range = $this->get_target_period_comparison($target, $range_days);
|
|
$unique_compare_7 = $this->get_target_unique_period_comparison($target, 7);
|
|
$unique_compare_30 = $this->get_target_unique_period_comparison($target, 30);
|
|
$unique_compare_range = $this->get_target_unique_period_comparison($target, $range_days);
|
|
$filename_slug = sanitize_title((string) ($target['label'] ?? 'page-stats'));
|
|
if ($filename_slug === '') {
|
|
$filename_slug = 'page-stats';
|
|
}
|
|
$filename = 'ansico-single-page-' . $filename_slug . '-' . wp_date('Y-m-d-H-i-s') . '.csv';
|
|
|
|
nocache_headers();
|
|
header('Content-Type: text/csv; charset=utf-8');
|
|
header('Content-Disposition: attachment; filename=' . $filename);
|
|
|
|
$output = fopen('php://output', 'w');
|
|
fputcsv($output, ['Selected URL', (string) ($target['url'] ?? '')]);
|
|
fputcsv($output, ['Label', (string) ($target['label'] ?? '')]);
|
|
fputcsv($output, ['Type', (string) (($target['kind'] ?? '') === 'singular' ? ($target['post_type'] ?? 'post') : ($target['page_type'] ?? 'page'))]);
|
|
fputcsv($output, ['Selected period start', $start_date]);
|
|
fputcsv($output, ['Selected period end', $end_date]);
|
|
fputcsv($output, ['Selected period views', (int) $this->get_target_period_views($target, $start_date, $end_date)]);
|
|
fputcsv($output, ['Selected period unique visitors', (int) $this->get_target_period_unique_visitors($target, $start_date, $end_date)]);
|
|
fputcsv($output, ['Previous comparable period views', (int) ($compare_range['previous_views'] ?? 0)]);
|
|
fputcsv($output, ['Previous comparable period unique visitors', (int) ($unique_compare_range['previous_views'] ?? 0)]);
|
|
fputcsv($output, ['Lifetime views', (int) $this->get_target_lifetime_views($target)]);
|
|
fputcsv($output, ['Views today', (int) $this->get_target_period_views($target, current_time('Y-m-d'), current_time('Y-m-d'))]);
|
|
fputcsv($output, ['Unique visitors today', (int) $this->get_target_period_unique_visitors($target, current_time('Y-m-d'), current_time('Y-m-d'))]);
|
|
fputcsv($output, ['Views last 7 days', (int) ($compare_7['current_views'] ?? 0)]);
|
|
fputcsv($output, ['Unique visitors last 7 days', (int) ($unique_compare_7['current_views'] ?? 0)]);
|
|
fputcsv($output, ['Previous 7 days', (int) ($compare_7['previous_views'] ?? 0)]);
|
|
fputcsv($output, ['Previous unique visitors 7 days', (int) ($unique_compare_7['previous_views'] ?? 0)]);
|
|
fputcsv($output, ['Views last 30 days', (int) ($compare_30['current_views'] ?? 0)]);
|
|
fputcsv($output, ['Unique visitors last 30 days', (int) ($unique_compare_30['current_views'] ?? 0)]);
|
|
fputcsv($output, ['Previous 30 days', (int) ($compare_30['previous_views'] ?? 0)]);
|
|
fputcsv($output, ['Previous unique visitors 30 days', (int) ($unique_compare_30['previous_views'] ?? 0)]);
|
|
fputcsv($output, []);
|
|
fputcsv($output, ['Date', 'Views', 'Unique visitors']);
|
|
foreach ($daily_rows as $row) {
|
|
fputcsv($output, [
|
|
(string) ($row['date'] ?? ''),
|
|
(int) ($row['views'] ?? 0),
|
|
(int) $this->get_target_period_unique_visitors($target, (string) ($row['date'] ?? ''), (string) ($row['date'] ?? '')),
|
|
]);
|
|
}
|
|
fclose($output);
|
|
exit;
|
|
}
|
|
|
|
public function handle_export_csv() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(esc_html__('You do not have permission to export statistics.', 'ansico-stat-plugin'));
|
|
}
|
|
|
|
check_admin_referer('ansico_stat_export_csv');
|
|
|
|
$filename = 'ansico-stats-' . wp_date('Y-m-d-H-i-s') . '.csv';
|
|
$install_month = $this->get_install_month_key();
|
|
$current_month = wp_date('Y-m', current_time('timestamp'));
|
|
|
|
nocache_headers();
|
|
header('Content-Type: text/csv; charset=utf-8');
|
|
header('Content-Disposition: attachment; filename=' . $filename);
|
|
|
|
$output = fopen('php://output', 'w');
|
|
fputcsv($output, ['Period type', 'Period', 'Rank', 'Title', 'Post ID', 'Post Type', 'Views', 'Lifetime Views', 'Permalink']);
|
|
|
|
$cursor = strtotime($install_month . '-01');
|
|
$end = strtotime($current_month . '-01');
|
|
while ($cursor <= $end) {
|
|
$month_key = wp_date('Y-m', $cursor);
|
|
$rows = $this->get_monthly_top_posts($month_key, 10);
|
|
foreach ($rows as $index => $row) {
|
|
$post_id = (int) $row['post_id'];
|
|
fputcsv($output, [
|
|
'month',
|
|
$month_key,
|
|
$index + 1,
|
|
get_the_title($post_id) ?: '(no title)',
|
|
$post_id,
|
|
get_post_type($post_id),
|
|
(int) $row['period_views'],
|
|
(int) get_post_meta($post_id, self::TOTAL_META_KEY, true),
|
|
get_permalink($post_id),
|
|
]);
|
|
}
|
|
$cursor = strtotime('+1 month', $cursor);
|
|
}
|
|
|
|
for ($year = $this->get_install_year(); $year <= (int) wp_date('Y', current_time('timestamp')); $year++) {
|
|
$rows = $this->get_yearly_top_posts($year, 10);
|
|
foreach ($rows as $index => $row) {
|
|
$post_id = (int) $row['post_id'];
|
|
fputcsv($output, [
|
|
'year',
|
|
$year,
|
|
$index + 1,
|
|
get_the_title($post_id) ?: '(no title)',
|
|
$post_id,
|
|
get_post_type($post_id),
|
|
(int) $row['period_views'],
|
|
(int) get_post_meta($post_id, self::TOTAL_META_KEY, true),
|
|
get_permalink($post_id),
|
|
]);
|
|
}
|
|
}
|
|
|
|
fclose($output);
|
|
exit;
|
|
}
|
|
|
|
public function handle_save_settings() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(esc_html__('You do not have permission to save these settings.', 'ansico-stat-plugin'));
|
|
}
|
|
|
|
check_admin_referer('ansico_stat_save_settings');
|
|
|
|
$allowed_post_types = $this->get_default_track_post_types();
|
|
$submitted_post_types = isset($_POST['track_post_types']) && is_array($_POST['track_post_types']) ? array_map('sanitize_key', wp_unslash($_POST['track_post_types'])) : [];
|
|
$submitted_post_types = array_values(array_intersect($allowed_post_types, $submitted_post_types));
|
|
|
|
$visitor_counting_mode = isset($_POST['visitor_counting_mode']) ? sanitize_key(wp_unslash($_POST['visitor_counting_mode'])) : 'count_all';
|
|
if (!in_array($visitor_counting_mode, ['count_all', 'exclude_admins', 'exclude_all_logged_in'], true)) {
|
|
$visitor_counting_mode = 'count_all';
|
|
}
|
|
|
|
$settings = [
|
|
'visitor_counting_mode' => $visitor_counting_mode,
|
|
'exclude_known_bots' => isset($_POST['exclude_known_bots']) ? 1 : 0,
|
|
'top_list_rows' => isset($_POST['top_list_rows']) ? max(1, min(100, (int) $_POST['top_list_rows'])) : 10,
|
|
'referral_rows' => isset($_POST['referral_rows']) ? max(1, min(200, (int) $_POST['referral_rows'])) : 25,
|
|
'revisit_minutes' => isset($_POST['revisit_minutes']) ? max(1, min(10080, (int) $_POST['revisit_minutes'])) : 30,
|
|
'frontend_label' => isset($_POST['frontend_label']) ? sanitize_text_field(wp_unslash($_POST['frontend_label'])) : 'Views',
|
|
'track_post_types' => $submitted_post_types,
|
|
'track_front_page' => isset($_POST['track_front_page']) ? 1 : 0,
|
|
'track_posts_page' => isset($_POST['track_posts_page']) ? 1 : 0,
|
|
'track_archives' => isset($_POST['track_archives']) ? 1 : 0,
|
|
'track_search' => isset($_POST['track_search']) ? 1 : 0,
|
|
'track_404' => isset($_POST['track_404']) ? 1 : 0,
|
|
];
|
|
|
|
update_option(self::OPTION_KEY, $settings, false);
|
|
|
|
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&settings-updated=true'));
|
|
exit;
|
|
}
|
|
|
|
public function handle_reset_all_views() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(esc_html__('You do not have permission to reset statistics.', 'ansico-stat-plugin'));
|
|
}
|
|
|
|
check_admin_referer('ansico_stat_reset_all');
|
|
|
|
global $wpdb;
|
|
$wpdb->query($wpdb->prepare(
|
|
"DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
|
self::TOTAL_META_KEY
|
|
));
|
|
$wpdb->query('TRUNCATE TABLE ' . self::daily_table_name());
|
|
$wpdb->query('TRUNCATE TABLE ' . self::post_daily_table_name());
|
|
$wpdb->query('TRUNCATE TABLE ' . self::page_daily_table_name());
|
|
$wpdb->query('TRUNCATE TABLE ' . self::referral_table_name());
|
|
$wpdb->query('TRUNCATE TABLE ' . self::dimension_table_name());
|
|
update_option(self::INSTALL_OPTION_KEY, current_time('Y-m-d'), false);
|
|
|
|
wp_safe_redirect(admin_url('admin.php?page=' . self::MENU_SLUG_SETTINGS . '&reset=success'));
|
|
exit;
|
|
}
|
|
|
|
public function register_admin_columns() {
|
|
if (!is_admin()) {
|
|
return;
|
|
}
|
|
|
|
$post_types = get_post_types(['show_ui' => true], 'names');
|
|
foreach ($post_types as $post_type) {
|
|
add_filter("manage_{$post_type}_posts_columns", [$this, 'add_view_columns']);
|
|
add_action("manage_{$post_type}_posts_custom_column", [$this, 'render_view_columns'], 10, 2);
|
|
add_filter("manage_edit-{$post_type}_sortable_columns", [$this, 'register_sortable_view_columns']);
|
|
}
|
|
|
|
add_filter('manage_pages_columns', [$this, 'add_view_columns']);
|
|
add_action('manage_pages_custom_column', [$this, 'render_view_columns'], 10, 2);
|
|
add_filter('manage_edit-page_sortable_columns', [$this, 'register_sortable_view_columns']);
|
|
}
|
|
|
|
public function add_view_columns($columns) {
|
|
$new_columns = [];
|
|
foreach ($columns as $key => $label) {
|
|
$new_columns[$key] = $label;
|
|
if ($key === 'title') {
|
|
$new_columns['ansico_lifetime_views'] = __('Post Views (Lifetime)', 'ansico-stat-plugin');
|
|
$new_columns['ansico_latest_month_views'] = __('Post Views (Latest Month)', 'ansico-stat-plugin');
|
|
}
|
|
}
|
|
|
|
if (!isset($new_columns['ansico_lifetime_views'])) {
|
|
$new_columns['ansico_lifetime_views'] = __('Post Views (Lifetime)', 'ansico-stat-plugin');
|
|
$new_columns['ansico_latest_month_views'] = __('Post Views (Latest Month)', 'ansico-stat-plugin');
|
|
}
|
|
|
|
return $new_columns;
|
|
}
|
|
|
|
public function render_view_columns($column, $post_id) {
|
|
if ($column === 'ansico_lifetime_views') {
|
|
echo esc_html(number_format_i18n((int) get_post_meta($post_id, self::TOTAL_META_KEY, true)));
|
|
}
|
|
|
|
if ($column === 'ansico_latest_month_views') {
|
|
echo esc_html(number_format_i18n($this->get_post_views_for_latest_month((int) $post_id)));
|
|
}
|
|
}
|
|
|
|
public function register_sortable_view_columns($columns) {
|
|
$columns['ansico_lifetime_views'] = 'ansico_lifetime_views';
|
|
$columns['ansico_latest_month_views'] = 'ansico_latest_month_views';
|
|
return $columns;
|
|
}
|
|
|
|
public function render_admin_view_filter() {
|
|
global $typenow, $pagenow;
|
|
if (!is_admin() || $pagenow !== 'edit.php' || empty($typenow)) {
|
|
return;
|
|
}
|
|
|
|
$post_type_object = get_post_type_object($typenow);
|
|
if (!$post_type_object || empty($post_type_object->show_ui)) {
|
|
return;
|
|
}
|
|
|
|
$selected = isset($_GET['ansico_view_filter']) ? sanitize_text_field(wp_unslash($_GET['ansico_view_filter'])) : '';
|
|
?>
|
|
<select name="ansico_view_filter">
|
|
<option value=""><?php echo esc_html__('View stats filter', 'ansico-stat-plugin'); ?></option>
|
|
<option value="lifetime_desc" <?php selected($selected, 'lifetime_desc'); ?>><?php echo esc_html__('Most viewed (lifetime)', 'ansico-stat-plugin'); ?></option>
|
|
<option value="lifetime_asc" <?php selected($selected, 'lifetime_asc'); ?>><?php echo esc_html__('Least viewed (lifetime)', 'ansico-stat-plugin'); ?></option>
|
|
<option value="month_desc" <?php selected($selected, 'month_desc'); ?>><?php echo esc_html__('Most viewed (latest month)', 'ansico-stat-plugin'); ?></option>
|
|
<option value="month_asc" <?php selected($selected, 'month_asc'); ?>><?php echo esc_html__('Least viewed (latest month)', 'ansico-stat-plugin'); ?></option>
|
|
</select>
|
|
<?php
|
|
}
|
|
|
|
public function handle_admin_list_sorting($query) {
|
|
if (!is_admin() || !$query->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();
|
|
}
|