2551 lines
125 KiB
PHP
2551 lines
125 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.0.0.3
|
||
|
|
* 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.0.0.3';
|
||
|
|
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 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_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_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,
|
||
|
|
PRIMARY KEY (post_id, stat_date),
|
||
|
|
KEY stat_date (stat_date),
|
||
|
|
KEY views (views)
|
||
|
|
) {$charset_collate};";
|
||
|
|
|
||
|
|
$sql_page_daily = "CREATE TABLE {$page_daily_table} (
|
||
|
|
page_key varchar(191) NOT NULL,
|
||
|
|
page_type varchar(32) NOT NULL,
|
||
|
|
object_id bigint(20) unsigned NULL DEFAULT NULL,
|
||
|
|
page_label varchar(255) NOT NULL,
|
||
|
|
page_url text NULL,
|
||
|
|
stat_date date NOT NULL,
|
||
|
|
views bigint(20) unsigned NOT NULL DEFAULT 0,
|
||
|
|
PRIMARY KEY (page_key, stat_date),
|
||
|
|
KEY stat_date (stat_date),
|
||
|
|
KEY page_type (page_type),
|
||
|
|
KEY object_id (object_id),
|
||
|
|
KEY views (views)
|
||
|
|
) {$charset_collate};";
|
||
|
|
|
||
|
|
$sql_referral = "CREATE TABLE {$referral_table} (
|
||
|
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||
|
|
stat_date date NOT NULL,
|
||
|
|
category varchar(32) NOT NULL,
|
||
|
|
source_url text NULL,
|
||
|
|
source_host varchar(191) NULL,
|
||
|
|
visits bigint(20) unsigned NOT NULL DEFAULT 0,
|
||
|
|
PRIMARY KEY (id),
|
||
|
|
UNIQUE KEY unique_referral (stat_date, category, source_host(191), source_url(191)),
|
||
|
|
KEY stat_date (stat_date),
|
||
|
|
KEY category (category),
|
||
|
|
KEY source_host (source_host)
|
||
|
|
) {$charset_collate};";
|
||
|
|
|
||
|
|
$sql_dimensions = "CREATE TABLE {$dimension_table} (
|
||
|
|
stat_date date NOT NULL,
|
||
|
|
dimension_type varchar(32) NOT NULL,
|
||
|
|
dimension_value varchar(191) NOT NULL,
|
||
|
|
views bigint(20) unsigned NOT NULL DEFAULT 0,
|
||
|
|
PRIMARY KEY (stat_date, dimension_type, dimension_value),
|
||
|
|
KEY dimension_type (dimension_type),
|
||
|
|
KEY views (views)
|
||
|
|
) {$charset_collate};";
|
||
|
|
|
||
|
|
dbDelta($sql_daily);
|
||
|
|
dbDelta($sql_post_daily);
|
||
|
|
dbDelta($sql_page_daily);
|
||
|
|
dbDelta($sql_referral);
|
||
|
|
dbDelta($sql_dimensions);
|
||
|
|
|
||
|
|
if (!get_option(self::INSTALL_OPTION_KEY)) {
|
||
|
|
update_option(self::INSTALL_OPTION_KEY, current_time('Y-m-d'), false);
|
||
|
|
}
|
||
|
|
update_option(self::SCHEMA_VERSION_OPTION_KEY, self::VERSION, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function deactivate() {
|
||
|
|
// Keep data on deactivation.
|
||
|
|
}
|
||
|
|
|
||
|
|
public function maybe_upgrade_schema() {
|
||
|
|
$stored_version = (string) get_option(self::SCHEMA_VERSION_OPTION_KEY, '');
|
||
|
|
if ($stored_version === self::VERSION) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
self::activate();
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
public function load_textdomain() {
|
||
|
|
load_plugin_textdomain('ansico-stat-plugin', false, dirname(plugin_basename(__FILE__)) . '/languages');
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function daily_table_name() {
|
||
|
|
global $wpdb;
|
||
|
|
return $wpdb->prefix . 'ansico_stat_daily';
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function post_daily_table_name() {
|
||
|
|
global $wpdb;
|
||
|
|
return $wpdb->prefix . 'ansico_stat_post_daily';
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function page_daily_table_name() {
|
||
|
|
global $wpdb;
|
||
|
|
return $wpdb->prefix . 'ansico_stat_page_daily';
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function referral_table_name() {
|
||
|
|
global $wpdb;
|
||
|
|
return $wpdb->prefix . 'ansico_stat_referrals';
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function dimension_table_name() {
|
||
|
|
global $wpdb;
|
||
|
|
return $wpdb->prefix . 'ansico_stat_dimensions';
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function get_default_track_post_types() {
|
||
|
|
$post_types = get_post_types(['public' => true], 'names');
|
||
|
|
$post_types = array_filter($post_types, function($post_type) {
|
||
|
|
return !in_array($post_type, ['attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block', 'wp_template', 'wp_template_part', 'wp_navigation', 'wp_global_styles'], true);
|
||
|
|
});
|
||
|
|
return array_values($post_types);
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function get_settings() {
|
||
|
|
$settings = get_option(self::OPTION_KEY, []);
|
||
|
|
if (!is_array($settings)) {
|
||
|
|
$settings = [];
|
||
|
|
}
|
||
|
|
|
||
|
|
$settings = wp_parse_args($settings, [
|
||
|
|
'visitor_counting_mode' => 'count_all',
|
||
|
|
'exclude_known_bots' => 1,
|
||
|
|
'top_list_rows' => 10,
|
||
|
|
'referral_rows' => 25,
|
||
|
|
'revisit_minutes' => 30,
|
||
|
|
'frontend_label' => 'Views',
|
||
|
|
'track_post_types' => $this->get_default_track_post_types(),
|
||
|
|
'track_front_page' => 1,
|
||
|
|
'track_posts_page' => 1,
|
||
|
|
'track_archives' => 1,
|
||
|
|
'track_search' => 1,
|
||
|
|
'track_404' => 1,
|
||
|
|
]);
|
||
|
|
|
||
|
|
if (!is_array($settings['track_post_types'])) {
|
||
|
|
$settings['track_post_types'] = $this->get_default_track_post_types();
|
||
|
|
}
|
||
|
|
|
||
|
|
$settings['track_post_types'] = array_values(array_filter(array_map('sanitize_key', $settings['track_post_types'])));
|
||
|
|
$settings['visitor_counting_mode'] = in_array($settings['visitor_counting_mode'], ['count_all', 'exclude_admins', 'exclude_all_logged_in'], true) ? $settings['visitor_counting_mode'] : 'count_all';
|
||
|
|
$settings['revisit_minutes'] = max(1, min(10080, (int) $settings['revisit_minutes']));
|
||
|
|
$settings['frontend_label'] = sanitize_text_field((string) $settings['frontend_label']);
|
||
|
|
if ($settings['frontend_label'] === '') {
|
||
|
|
$settings['frontend_label'] = 'Views';
|
||
|
|
}
|
||
|
|
|
||
|
|
return $settings;
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function get_cookie_ttl() {
|
||
|
|
$settings = $this->get_settings();
|
||
|
|
return max(60, (int) $settings['revisit_minutes'] * 60);
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function should_exclude_current_user() {
|
||
|
|
$settings = $this->get_settings();
|
||
|
|
$mode = $settings['visitor_counting_mode'] ?? 'count_all';
|
||
|
|
|
||
|
|
if ($mode === 'exclude_all_logged_in' && is_user_logged_in()) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($mode === 'exclude_admins' && current_user_can('manage_options')) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function is_tracking_enabled_for_context(array $page_context) {
|
||
|
|
$settings = $this->get_settings();
|
||
|
|
$page_type = (string) ($page_context['page_type'] ?? '');
|
||
|
|
|
||
|
|
if (($page_context['kind'] ?? '') === 'singular') {
|
||
|
|
$post_type = sanitize_key((string) ($page_context['post_type'] ?? get_post_type((int) ($page_context['post_id'] ?? 0))));
|
||
|
|
return in_array($post_type, $settings['track_post_types'], true);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($page_type === 'front_page') {
|
||
|
|
return !empty($settings['track_front_page']);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($page_type === 'home') {
|
||
|
|
return !empty($settings['track_posts_page']);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($page_type === 'search') {
|
||
|
|
return !empty($settings['track_search']);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($page_type === '404') {
|
||
|
|
return !empty($settings['track_404']);
|
||
|
|
}
|
||
|
|
|
||
|
|
return !empty($settings['track_archives']);
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function get_frontend_icon_markup() {
|
||
|
|
return '<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;
|
||
|
|
}
|
||
|
|
|
||
|
|
$cookie_name = self::COOKIE_PREFIX . md5($page_context['cookie_key']);
|
||
|
|
if (isset($_COOKIE[$cookie_name])) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$referer_data = $this->get_referer_data();
|
||
|
|
if ($page_context['kind'] === 'singular') {
|
||
|
|
$this->increment_post_views((int) $page_context['post_id'], $referer_data);
|
||
|
|
} else {
|
||
|
|
$this->increment_non_singular_views($page_context, $referer_data);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!headers_sent()) {
|
||
|
|
setcookie($cookie_name, '1', time() + $this->get_cookie_ttl(), COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true);
|
||
|
|
$_COOKIE[$cookie_name] = '1';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function is_trackable_request() {
|
||
|
|
return is_singular() || is_404() || is_home() || is_front_page() || is_post_type_archive() || is_category() || is_tag() || is_tax() || is_author() || is_date() || is_search() || is_archive();
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function normalize_request_uri() {
|
||
|
|
$uri = isset($_SERVER['REQUEST_URI']) ? (string) wp_unslash($_SERVER['REQUEST_URI']) : '/';
|
||
|
|
$path = wp_parse_url($uri, PHP_URL_PATH);
|
||
|
|
$path = is_string($path) && $path !== '' ? $path : '/';
|
||
|
|
return untrailingslashit($path) ?: '/';
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function get_current_page_context() {
|
||
|
|
if (is_admin()) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (is_singular()) {
|
||
|
|
$post = $this->get_current_singular_post();
|
||
|
|
if (!$post) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return [
|
||
|
|
'kind' => 'singular',
|
||
|
|
'post_id' => (int) $post->ID,
|
||
|
|
'post_type' => sanitize_key((string) $post->post_type),
|
||
|
|
'cookie_key' => 'post:' . (int) $post->ID,
|
||
|
|
'page_key' => 'post:' . (int) $post->ID,
|
||
|
|
'page_type' => sanitize_key((string) $post->post_type),
|
||
|
|
'object_id' => (int) $post->ID,
|
||
|
|
'label' => get_the_title($post->ID) ?: __('(no title)', 'ansico-stat-plugin'),
|
||
|
|
'url' => get_permalink($post->ID) ?: '',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$request_path = $this->normalize_request_uri();
|
||
|
|
$base = [
|
||
|
|
'kind' => 'archive',
|
||
|
|
'cookie_key' => $request_path,
|
||
|
|
'page_key' => '',
|
||
|
|
'page_type' => 'archive',
|
||
|
|
'object_id' => 0,
|
||
|
|
'label' => '',
|
||
|
|
'url' => home_url($request_path . '/'),
|
||
|
|
];
|
||
|
|
|
||
|
|
if (is_404()) {
|
||
|
|
$base['page_type'] = '404';
|
||
|
|
$base['page_key'] = '404:' . md5($request_path);
|
||
|
|
$base['label'] = sprintf(__('404: %s', 'ansico-stat-plugin'), $request_path);
|
||
|
|
$base['url'] = home_url($request_path === '/' ? '/' : $request_path . '/');
|
||
|
|
return $base;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (is_category() || is_tag() || is_tax()) {
|
||
|
|
$term = get_queried_object();
|
||
|
|
if (!$term || empty($term->term_id)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
$taxonomy = is_string($term->taxonomy ?? '') ? $term->taxonomy : 'taxonomy';
|
||
|
|
$base['page_type'] = $taxonomy;
|
||
|
|
$base['page_key'] = 'term:' . $taxonomy . ':' . (int) $term->term_id;
|
||
|
|
$base['cookie_key'] = $base['page_key'];
|
||
|
|
$base['object_id'] = (int) $term->term_id;
|
||
|
|
$base['label'] = single_term_title('', false) ?: ($term->name ?? $request_path);
|
||
|
|
$base['url'] = get_term_link($term);
|
||
|
|
return $base;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (is_author()) {
|
||
|
|
$author = get_queried_object();
|
||
|
|
if (!$author || empty($author->ID)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
$base['page_type'] = 'author';
|
||
|
|
$base['page_key'] = 'author:' . (int) $author->ID;
|
||
|
|
$base['cookie_key'] = $base['page_key'];
|
||
|
|
$base['object_id'] = (int) $author->ID;
|
||
|
|
$base['label'] = get_the_author_meta('display_name', (int) $author->ID) ?: $request_path;
|
||
|
|
$base['url'] = get_author_posts_url((int) $author->ID);
|
||
|
|
return $base;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (is_post_type_archive()) {
|
||
|
|
$post_type = get_query_var('post_type');
|
||
|
|
$post_type = is_array($post_type) ? reset($post_type) : $post_type;
|
||
|
|
$post_type = sanitize_key((string) $post_type);
|
||
|
|
if ($post_type === '') {
|
||
|
|
$post_type = 'archive';
|
||
|
|
}
|
||
|
|
$base['page_type'] = 'post_type_archive';
|
||
|
|
$base['page_key'] = 'post_type_archive:' . $post_type;
|
||
|
|
$base['cookie_key'] = $base['page_key'];
|
||
|
|
$base['label'] = post_type_archive_title('', false) ?: $request_path;
|
||
|
|
$archive_link = get_post_type_archive_link($post_type);
|
||
|
|
$base['url'] = $archive_link ?: $base['url'];
|
||
|
|
return $base;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (is_home() || is_front_page()) {
|
||
|
|
$base['page_type'] = is_front_page() ? 'front_page' : 'home';
|
||
|
|
$base['page_key'] = $base['page_type'];
|
||
|
|
$base['cookie_key'] = $base['page_key'];
|
||
|
|
$base['label'] = is_front_page() ? __('Front page', 'ansico-stat-plugin') : __('Posts page', 'ansico-stat-plugin');
|
||
|
|
$base['url'] = home_url('/');
|
||
|
|
return $base;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (is_date()) {
|
||
|
|
$base['page_type'] = 'date';
|
||
|
|
$base['page_key'] = 'date:' . md5($request_path);
|
||
|
|
$base['label'] = wp_get_document_title() ?: $request_path;
|
||
|
|
return $base;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (is_search()) {
|
||
|
|
$search_query = get_search_query();
|
||
|
|
$base['page_type'] = 'search';
|
||
|
|
$base['page_key'] = 'search:' . md5((string) $search_query);
|
||
|
|
$base['cookie_key'] = 'search:' . md5((string) $search_query);
|
||
|
|
$base['label'] = sprintf(__('Search: %s', 'ansico-stat-plugin'), $search_query ?: __('(empty)', 'ansico-stat-plugin'));
|
||
|
|
$base['url'] = get_search_link($search_query);
|
||
|
|
return $base;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (is_archive()) {
|
||
|
|
$base['page_type'] = 'archive';
|
||
|
|
$base['page_key'] = 'archive:' . md5($request_path);
|
||
|
|
$base['label'] = wp_get_document_title() ?: $request_path;
|
||
|
|
return $base;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function increment_post_views(int $post_id, array $referer_data = []) {
|
||
|
|
global $wpdb;
|
||
|
|
|
||
|
|
$today = current_time('Y-m-d');
|
||
|
|
$daily_table = self::daily_table_name();
|
||
|
|
$post_daily_table = self::post_daily_table_name();
|
||
|
|
$page_daily_table = self::page_daily_table_name();
|
||
|
|
$referral_table = self::referral_table_name();
|
||
|
|
|
||
|
|
$current_total = (int) get_post_meta($post_id, self::TOTAL_META_KEY, true);
|
||
|
|
update_post_meta($post_id, self::TOTAL_META_KEY, $current_total + 1);
|
||
|
|
|
||
|
|
$wpdb->query($wpdb->prepare(
|
||
|
|
"INSERT INTO {$daily_table} (stat_date, views)
|
||
|
|
VALUES (%s, 1)
|
||
|
|
ON DUPLICATE KEY UPDATE views = views + 1",
|
||
|
|
$today
|
||
|
|
));
|
||
|
|
|
||
|
|
$wpdb->query($wpdb->prepare(
|
||
|
|
"INSERT INTO {$post_daily_table} (post_id, stat_date, views)
|
||
|
|
VALUES (%d, %s, 1)
|
||
|
|
ON DUPLICATE KEY UPDATE views = views + 1",
|
||
|
|
$post_id,
|
||
|
|
$today
|
||
|
|
));
|
||
|
|
|
||
|
|
$category = isset($referer_data['category']) ? sanitize_key($referer_data['category']) : 'direct';
|
||
|
|
$source_url = isset($referer_data['source_url']) ? esc_url_raw((string) $referer_data['source_url']) : '';
|
||
|
|
$source_host = isset($referer_data['source_host']) ? sanitize_text_field((string) $referer_data['source_host']) : '';
|
||
|
|
|
||
|
|
$wpdb->query($wpdb->prepare(
|
||
|
|
"INSERT INTO {$referral_table} (stat_date, category, source_url, source_host, visits)
|
||
|
|
VALUES (%s, %s, %s, %s, 1)
|
||
|
|
ON DUPLICATE KEY UPDATE visits = visits + 1",
|
||
|
|
$today,
|
||
|
|
$category,
|
||
|
|
$source_url,
|
||
|
|
$source_host
|
||
|
|
));
|
||
|
|
|
||
|
|
$this->increment_dimension_views($today);
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function increment_non_singular_views(array $page_context, array $referer_data = []) {
|
||
|
|
global $wpdb;
|
||
|
|
|
||
|
|
$today = current_time('Y-m-d');
|
||
|
|
$daily_table = self::daily_table_name();
|
||
|
|
$page_daily_table = self::page_daily_table_name();
|
||
|
|
$referral_table = self::referral_table_name();
|
||
|
|
|
||
|
|
$wpdb->query($wpdb->prepare(
|
||
|
|
"INSERT INTO {$daily_table} (stat_date, views)
|
||
|
|
VALUES (%s, 1)
|
||
|
|
ON DUPLICATE KEY UPDATE views = views + 1",
|
||
|
|
$today
|
||
|
|
));
|
||
|
|
|
||
|
|
$page_key = substr(sanitize_text_field((string) $page_context['page_key']), 0, 191);
|
||
|
|
$page_type = substr(sanitize_key((string) $page_context['page_type']), 0, 32);
|
||
|
|
$object_id = !empty($page_context['object_id']) ? (int) $page_context['object_id'] : null;
|
||
|
|
$label = sanitize_text_field((string) $page_context['label']);
|
||
|
|
$url = isset($page_context['url']) ? esc_url_raw((string) $page_context['url']) : '';
|
||
|
|
|
||
|
|
$wpdb->query($wpdb->prepare(
|
||
|
|
"INSERT INTO {$page_daily_table} (page_key, page_type, object_id, page_label, page_url, stat_date, views)
|
||
|
|
VALUES (%s, %s, %s, %s, %s, %s, 1)
|
||
|
|
ON DUPLICATE KEY UPDATE views = views + 1, page_label = VALUES(page_label), page_url = VALUES(page_url)",
|
||
|
|
$page_key,
|
||
|
|
$page_type,
|
||
|
|
$object_id,
|
||
|
|
$label,
|
||
|
|
$url,
|
||
|
|
$today
|
||
|
|
));
|
||
|
|
|
||
|
|
$category = isset($referer_data['category']) ? sanitize_key($referer_data['category']) : 'direct';
|
||
|
|
$source_url = isset($referer_data['source_url']) ? esc_url_raw((string) $referer_data['source_url']) : '';
|
||
|
|
$source_host = isset($referer_data['source_host']) ? sanitize_text_field((string) $referer_data['source_host']) : '';
|
||
|
|
|
||
|
|
$wpdb->query($wpdb->prepare(
|
||
|
|
"INSERT INTO {$referral_table} (stat_date, category, source_url, source_host, visits)
|
||
|
|
VALUES (%s, %s, %s, %s, 1)
|
||
|
|
ON DUPLICATE KEY UPDATE visits = visits + 1",
|
||
|
|
$today,
|
||
|
|
$category,
|
||
|
|
$source_url,
|
||
|
|
$source_host
|
||
|
|
));
|
||
|
|
|
||
|
|
$this->increment_dimension_views($today);
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function get_non_singular_lifetime_views(string $page_key) {
|
||
|
|
global $wpdb;
|
||
|
|
$table = self::page_daily_table_name();
|
||
|
|
$views = $wpdb->get_var($wpdb->prepare(
|
||
|
|
"SELECT SUM(views) FROM {$table} WHERE page_key = %s",
|
||
|
|
$page_key
|
||
|
|
));
|
||
|
|
return (int) $views;
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function get_current_singular_post() {
|
||
|
|
if (is_admin() || !is_singular()) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$post = get_queried_object();
|
||
|
|
if (!$this->is_trackable_post($post)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $post;
|
||
|
|
}
|
||
|
|
|
||
|
|
protected function get_admin_view_markup(array $page_context) {
|
||
|
|
if (!$this->is_tracking_enabled_for_context($page_context)) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
$views = 0;
|
||
|
|
if (($page_context['kind'] ?? '') === 'singular' && !empty($page_context['post_id'])) {
|
||
|
|
$views = (int) get_post_meta((int) $page_context['post_id'], self::TOTAL_META_KEY, true);
|
||
|
|
} elseif (!empty($page_context['page_key'])) {
|
||
|
|
$views = $this->get_non_singular_lifetime_views((string) $page_context['page_key']);
|
||
|
|
}
|
||
|
|
|
||
|
|
$settings = $this->get_settings();
|
||
|
|
$label = sanitize_text_field((string) ($settings['frontend_label'] ?? 'Views'));
|
||
|
|
if ($label === '') {
|
||
|
|
$label = 'Views';
|
||
|
|
}
|
||
|
|
|
||
|
|
$markup = '<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,
|
||
|
|
__('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_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
|
||
|
|
}
|
||
|
|
|
||
|
|
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_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();
|
||
|
|
}
|