/
www
/
wwwroot
/
fxdst.com
/
wp-content
/
mu-plugins
/
Upload File
HOME
<?php /** * Plugin Name: FXDST Analytics * Description: Local WordPress analytics for sources, keywords, pages, clicks, and countries. Does not store plaintext IP addresses. * Version: 1.3.7 * Author: FXDST */ if (!defined('ABSPATH')) { exit; } if (class_exists('FXDST_Analytics', false)) { FXDST_Analytics::boot(); return; } final class FXDST_Analytics { const VERSION = '1.3.7'; const OPTION_VERSION = 'fxdst_analytics_version'; const NONCE_ACTION = 'fxdst_analytics_track'; public static function boot() { static $booted = false; if ($booted) { return; } $booted = true; add_action('init', array(__CLASS__, 'maybe_install'), 1); add_action('wp_footer', array(__CLASS__, 'render_tracker'), 99); add_action('wp_ajax_fxdst_track', array(__CLASS__, 'track_ajax')); add_action('wp_ajax_nopriv_fxdst_track', array(__CLASS__, 'track_ajax')); add_action('wp_dashboard_setup', array(__CLASS__, 'setup_dashboard')); add_action('welcome_panel', array(__CLASS__, 'render_welcome_panel')); add_action('admin_menu', array(__CLASS__, 'register_admin_page')); add_filter('manage_post_posts_columns', array(__CLASS__, 'add_admin_views_column')); add_filter('manage_sites_posts_columns', array(__CLASS__, 'add_admin_views_column')); add_filter('manage_edit-post_sortable_columns', array(__CLASS__, 'add_admin_views_sortable_column')); add_filter('manage_edit-sites_sortable_columns', array(__CLASS__, 'add_admin_views_sortable_column')); add_action('manage_post_posts_custom_column', array(__CLASS__, 'render_admin_views_column'), 10, 2); add_action('manage_sites_posts_custom_column', array(__CLASS__, 'render_admin_views_column'), 10, 2); add_filter('posts_clauses', array(__CLASS__, 'sort_admin_views_column_clauses'), 10, 2); } private static function zh($escaped) { $decoded = json_decode('"' . $escaped . '"'); return is_string($decoded) ? $decoded : $escaped; } private static function table_name() { global $wpdb; return $wpdb->prefix . 'fxdst_analytics_events'; } public static function maybe_install() { if (get_option(self::OPTION_VERSION) === self::VERSION) { return; } global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $table = self::table_name(); $charset = $wpdb->get_charset_collate(); $sql = "CREATE TABLE {$table} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, event_time datetime NOT NULL, event_type varchar(20) NOT NULL DEFAULT 'pageview', visitor_hash char(64) NOT NULL DEFAULT '', session_hash char(64) NOT NULL DEFAULT '', ip_hash char(64) NOT NULL DEFAULT '', country_code varchar(2) NOT NULL DEFAULT '', country_name varchar(80) NOT NULL DEFAULT '', page_url text NULL, page_path varchar(255) NOT NULL DEFAULT '', page_title varchar(255) NOT NULL DEFAULT '', referrer text NULL, ref_host varchar(191) NOT NULL DEFAULT '', source varchar(80) NOT NULL DEFAULT '', medium varchar(80) NOT NULL DEFAULT '', campaign varchar(191) NOT NULL DEFAULT '', keyword varchar(191) NOT NULL DEFAULT '', target_url text NULL, target_host varchar(191) NOT NULL DEFAULT '', target_text varchar(255) NOT NULL DEFAULT '', post_id bigint(20) unsigned NOT NULL DEFAULT 0, term_id bigint(20) unsigned NOT NULL DEFAULT 0, device varchar(20) NOT NULL DEFAULT '', browser varchar(40) NOT NULL DEFAULT '', user_agent_hash char(64) NOT NULL DEFAULT '', PRIMARY KEY (id), KEY event_time (event_time), KEY event_type (event_type), KEY visitor_hash (visitor_hash), KEY country_code (country_code), KEY source (source), KEY medium (medium), KEY ref_host (ref_host), KEY keyword (keyword), KEY page_path (page_path), KEY target_host (target_host) ) {$charset};"; dbDelta($sql); update_option(self::OPTION_VERSION, self::VERSION, false); } public static function render_tracker() { if (is_admin() || current_user_can('manage_options')) { return; } $config = array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce(self::NONCE_ACTION), 'postId' => is_singular(array('post', 'sites')) ? get_queried_object_id() : 0, ); ?> <script id="fxdst-analytics-js"> (function(){ var cfg = window.FXDST_ANALYTICS || <?php echo wp_json_encode($config); ?>; if (!cfg || !cfg.ajaxUrl) return; function randId(){return 'fx' + Date.now().toString(36) + Math.random().toString(36).slice(2, 12);} function getStore(key, ttlMinutes){ try { var now = Date.now(); var raw = localStorage.getItem(key); if (raw) { var item = JSON.parse(raw); if (!item.expires || item.expires > now) { if (ttlMinutes) { item.expires = now + ttlMinutes * 60000; localStorage.setItem(key, JSON.stringify(item)); } return item.value; } } var value = randId(); localStorage.setItem(key, JSON.stringify({value:value, expires: ttlMinutes ? now + ttlMinutes * 60000 : 0})); return value; } catch(e) { return randId(); } } var visitorId = getStore('fxdst_vid', 0); var sessionId = getStore('fxdst_sid', 30); function send(type, extra){ extra = extra || {}; var data = new URLSearchParams(); data.append('action', 'fxdst_track'); data.append('_ajax_nonce', cfg.nonce); data.append('event_type', type); data.append('vid', visitorId); data.append('sid', sessionId); data.append('page_url', location.href); data.append('page_title', document.title || ''); data.append('referrer', document.referrer || ''); Object.keys(extra).forEach(function(k){ data.append(k, extra[k] == null ? '' : String(extra[k])); }); if (cfg.postId) data.append('post_id', cfg.postId); if (navigator.sendBeacon) { navigator.sendBeacon(cfg.ajaxUrl, data); return; } fetch(cfg.ajaxUrl, {method:'POST', body:data, credentials:'same-origin', keepalive:true}).catch(function(){}); } window.addEventListener('load', function(){ send('pageview'); }); document.addEventListener('click', function(ev){ var el = ev.target && ev.target.closest ? ev.target.closest('a[href]') : null; if (!el) return; var href = el.href || ''; if (!href || /^(javascript:|mailto:|tel:)/i.test(href)) return; var text = (el.innerText || el.getAttribute('title') || '').replace(/\s+/g, ' ').trim().slice(0, 160); send('click', {target_url: href, target_text: text}); }, true); document.addEventListener('submit', function(ev){ var form = ev.target; if (!form || !form.querySelector) return; var input = form.querySelector('input[name="s"], input[type="search"]'); if (!input || !input.value) return; send('search', {keyword: input.value.slice(0, 120), target_url: form.action || location.href}); }, true); })(); </script> <?php } public static function track_ajax() { check_ajax_referer(self::NONCE_ACTION); $event_type = self::clean_key($_POST['event_type'] ?? 'pageview'); if (!in_array($event_type, array('pageview', 'click', 'search'), true)) { $event_type = 'pageview'; } $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; if (self::is_bot($ua)) { wp_send_json_success(array('ignored' => true)); } $page_url = self::clean_url($_POST['page_url'] ?? ''); $target_url = self::clean_url($_POST['target_url'] ?? ''); $referrer = self::clean_url($_POST['referrer'] ?? ''); $page_parts = self::parse_url_parts($page_url); $target_parts = self::parse_url_parts($target_url); $ref_parts = self::parse_url_parts($referrer); $utm = self::parse_query_params($page_url); $ref_data = self::classify_referrer($referrer, $utm); $keyword = self::clean_text($_POST['keyword'] ?? '', 191); if ($keyword === '') { $keyword = self::extract_keyword($referrer); } if ($keyword === '' && isset($utm['utm_term'])) { $keyword = self::clean_text($utm['utm_term'], 191); } if ($keyword === '' && isset($page_parts['query']['s'])) { $keyword = self::clean_text($page_parts['query']['s'], 191); } $country = self::detect_country(self::client_ip()); global $wpdb; self::maybe_install(); $wpdb->insert(self::table_name(), array( 'event_time' => current_time('mysql'), 'event_type' => $event_type, 'visitor_hash' => self::hash_value(self::clean_text($_POST['vid'] ?? '', 120)), 'session_hash' => self::hash_value(self::clean_text($_POST['sid'] ?? '', 120)), 'ip_hash' => self::hash_value(self::client_ip()), 'country_code' => $country['code'], 'country_name' => $country['name'], 'page_url' => $page_url, 'page_path' => self::clean_text($page_parts['path'] ?? '/', 255), 'page_title' => self::clean_text($_POST['page_title'] ?? '', 255), 'referrer' => $referrer, 'ref_host' => self::clean_text($ref_parts['host'] ?? '', 191), 'source' => self::clean_text($ref_data['source'], 80), 'medium' => self::clean_text($ref_data['medium'], 80), 'campaign' => self::clean_text($utm['utm_campaign'] ?? '', 191), 'keyword' => $keyword, 'target_url' => $target_url, 'target_host' => self::clean_text($target_parts['host'] ?? '', 191), 'target_text' => self::clean_text($_POST['target_text'] ?? '', 255), 'post_id' => self::tracked_post_id(), 'term_id' => 0, 'device' => self::detect_device($ua), 'browser' => self::detect_browser($ua), 'user_agent_hash' => self::hash_value($ua), )); wp_send_json_success(array('stored' => true)); } public static function add_admin_views_column($columns) { if (isset($columns['fxdst_views'])) { return $columns; } $new = array(); $inserted = false; foreach ($columns as $key => $label) { $new[$key] = $label; if ($key === 'author') { $new['fxdst_views'] = self::zh('\u8bbf\u95ee'); $inserted = true; } } if ($inserted) { return $new; } $new = array(); foreach ($columns as $key => $label) { if ($key === 'date') { $new['fxdst_views'] = self::zh('\u8bbf\u95ee'); } $new[$key] = $label; } if (!isset($new['fxdst_views'])) { $new['fxdst_views'] = self::zh('\u8bbf\u95ee'); } return $new; } public static function render_admin_views_column($column, $post_id) { if ($column !== 'fxdst_views') { return; } echo esc_html(number_format_i18n(self::post_pageviews((int)$post_id))); } public static function add_admin_views_sortable_column($columns) { $columns['fxdst_views'] = array('fxdst_views', true, self::zh('\u8bbf\u95ee'), self::zh('\u6309\u8bbf\u95ee\u6392\u5e8f'), 'desc'); return $columns; } public static function sort_admin_views_column_clauses($clauses, $query) { if (!is_admin() || !is_object($query) || $query->get('orderby') !== 'fxdst_views') { return $clauses; } $post_type = $query->get('post_type') ?: 'post'; if (is_array($post_type)) { $post_type = reset($post_type); } if (!in_array($post_type, array('post', 'sites'), true) || !self::analytics_table_exists()) { return $clauses; } global $wpdb; $table = esc_sql(self::table_name()); $alias = 'fxdst_views_sort'; if (strpos($clauses['join'], $alias) === false) { $events_sql = " SELECT post_id AS object_id FROM {$table} WHERE event_type = 'pageview' AND post_id > 0 UNION ALL SELECT CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(page_path, '/', -1), '.', 1) AS UNSIGNED) AS object_id FROM {$table} WHERE event_type = 'pageview' AND post_id = 0 AND page_path REGEXP '^/[0-9]+[.]html/?$' UNION ALL SELECT CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(page_path, '/', -1), '.', 1) AS UNSIGNED) AS object_id FROM {$table} WHERE event_type = 'pageview' AND post_id = 0 AND page_path REGEXP '^/sites/[0-9]+[.]html/?$' "; $clauses['join'] .= " LEFT JOIN ( SELECT object_id, COUNT(*) AS total FROM ({$events_sql}) AS fxdst_view_events WHERE object_id > 0 GROUP BY object_id ) AS {$alias} ON {$alias}.object_id = {$wpdb->posts}.ID"; } $order = strtoupper((string)$query->get('order')) === 'ASC' ? 'ASC' : 'DESC'; $clauses['orderby'] = "COALESCE({$alias}.total, 0) {$order}, {$wpdb->posts}.post_date DESC"; return $clauses; } public static function setup_dashboard() { if (!current_user_can('manage_options')) { return; } remove_action('welcome_panel', 'wp_welcome_panel'); } public static function render_welcome_panel() { if (!current_user_can('manage_options')) { return; } self::admin_style(); $stats = self::get_stats(7); $top_source = !empty($stats['sources']) ? $stats['sources'][0] : null; $top_country = !empty($stats['countries']) ? $stats['countries'][0] : null; $top_click = !empty($stats['clicks']) ? $stats['clicks'][0] : null; $source_text = $top_source ? (self::source_label($top_source->source) . ' / ' . self::medium_label($top_source->medium)) : '-'; $country_text = $top_country ? self::country_label($top_country) : '-'; $click_text = $top_click ? self::shorten($top_click->target_text ?: $top_click->target_url, 32) : '-'; echo '<div class="fxdst-welcome-bar">'; echo '<div class="fxdst-welcome-title"><strong>' . esc_html(self::zh('\u7ad9\u957f\u7edf\u8ba1')) . '</strong><span>' . esc_html(self::zh('\u8fd1 7 \u5929\u8fd0\u8425\u6570\u636e\uff0c\u7528\u4e8e\u4f18\u5316\u5de5\u5177\u6536\u5f55\u548c\u9996\u9875\u6392\u5e8f')) . '</span></div>'; echo '<div class="fxdst-welcome-metrics">'; self::welcome_metric(self::zh('\u4eca\u65e5\u8bbf\u95ee'), number_format_i18n((int)$stats['today_pageviews']), self::zh('\u6d4f\u89c8\u91cf')); self::welcome_metric(self::zh('\u4eca\u65e5\u8bbf\u5ba2'), number_format_i18n((int)$stats['today_visitors']), self::zh('\u8bbf\u5ba2')); self::welcome_metric(self::zh('\u4e3b\u8981\u6765\u6e90'), $source_text, $top_source ? number_format_i18n((int)$top_source->total) . self::zh(' \u6b21\u8bbf\u95ee') : '-'); self::welcome_metric(self::zh('\u4e3b\u8981\u56fd\u5bb6/\u5730\u533a'), $country_text, $top_country ? number_format_i18n((int)$top_country->total) . self::zh(' \u6b21\u8bbf\u95ee') : '-'); self::welcome_metric(self::zh('\u70ed\u95e8\u70b9\u51fb'), $click_text, $top_click ? number_format_i18n((int)$top_click->total) . self::zh(' \u6b21\u70b9\u51fb') : '-'); echo '</div>'; echo '<a class="button button-primary fxdst-welcome-link" href="' . esc_url(admin_url('index.php?page=fxdst-analytics')) . '">' . esc_html(self::zh('\u67e5\u770b\u5b8c\u6574\u7edf\u8ba1')) . '</a>'; echo '</div>'; } private static function welcome_metric($label, $value, $hint) { echo '<div class="fxdst-welcome-metric"><span>' . esc_html($label) . '</span><strong>' . esc_html($value) . '</strong><em>' . esc_html($hint) . '</em></div>'; } public static function register_admin_page() { add_dashboard_page(self::zh('\u7ad9\u957f\u7edf\u8ba1'), self::zh('\u7ad9\u957f\u7edf\u8ba1'), 'manage_options', 'fxdst-analytics', array(__CLASS__, 'render_admin_page')); } public static function render_admin_page() { $days = isset($_GET['days']) ? absint($_GET['days']) : 30; if (!in_array($days, array(7, 30, 90), true)) { $days = 30; } echo '<div class="wrap"><h1>' . esc_html(self::zh('\u7ad9\u957f\u7edf\u8ba1')) . '</h1><p>' . esc_html(self::zh('\u7528\u4e8e\u8fd0\u8425\u5206\u6790\uff1a\u6765\u6e90\u3001\u5173\u952e\u8bcd\u3001\u8bbf\u95ee\u9875\u9762\u548c\u70b9\u51fb\u94fe\u63a5\u3002\u4e0d\u4fdd\u5b58\u660e\u6587 IP\u3002')) . '</p>'; echo '<div class="fxdst-range-tabs">'; foreach (array(7, 30, 90) as $d) { $class = 'button fxdst-range-button' . ($d === $days ? ' button-primary is-active' : ''); $aria = $d === $days ? ' aria-current="page"' : ''; $url = add_query_arg(array('page' => 'fxdst-analytics', 'days' => $d), admin_url('index.php')); echo '<a class="' . esc_attr($class) . '"' . $aria . ' href="' . esc_url($url) . '">' . esc_html(self::zh('\u8fd1 ') . $d . self::zh(' \u5929')) . '</a>'; } echo '<span class="fxdst-range-current">' . esc_html(self::zh('\u5f53\u524d\u533a\u95f4\uff1a\u8fd1 ') . $days . self::zh(' \u5929')) . '</span></div>'; self::render_admin_stats($days); echo '</div>'; } private static function render_admin_stats($days) { self::admin_style(); $stats = self::get_stats($days); self::render_range_note($stats, $days); echo '<div class="fxdst-stat-grid">'; self::metric_card(self::zh('\u4eca\u65e5\u8bbf\u95ee'), $stats['today_pageviews'], self::zh('\u6d4f\u89c8\u91cf')); self::metric_card(self::zh('\u4eca\u65e5\u8bbf\u5ba2'), $stats['today_visitors'], self::zh('\u8bbf\u5ba2')); self::metric_card(self::zh('\u8fd1 ') . $days . self::zh(' \u5929\u8bbf\u95ee'), $stats['period_pageviews'], self::zh('\u6d4f\u89c8\u91cf')); self::metric_card(self::zh('\u8fd1 ') . $days . self::zh(' \u5929\u70b9\u51fb'), $stats['period_clicks'], self::zh('\u70b9\u51fb\u91cf')); echo '</div><div class="fxdst-stat-tables">'; self::render_table(self::zh('\u6765\u6e90\u6e20\u9053'), $stats['sources'], array(self::zh('\u6765\u6e90'), self::zh('\u5a92\u4ecb'), self::zh('\u8bbf\u95ee')), 'source'); self::render_table(self::zh('\u6765\u6e90\u56fd\u5bb6/\u5730\u533a'), $stats['countries'], array(self::zh('\u56fd\u5bb6/\u5730\u533a'), self::zh('\u4ee3\u7801'), self::zh('\u8bbf\u95ee')), 'country'); self::render_table(self::zh('\u641c\u7d22\u5173\u952e\u8bcd'), $stats['keywords'], array(self::zh('\u5173\u952e\u8bcd'), self::zh('\u8bbf\u95ee')), 'keyword'); self::render_table(self::zh('\u70ed\u95e8\u9875\u9762'), $stats['pages'], array(self::zh('\u9875\u9762'), self::zh('\u8bbf\u95ee')), 'page'); self::render_table(self::zh('\u70b9\u51fb\u94fe\u63a5'), $stats['clicks'], array(self::zh('\u94fe\u63a5'), self::zh('\u70b9\u51fb')), 'click'); self::render_recent($stats['recent']); echo '</div>'; } private static function render_range_note($stats, $days) { $range = self::format_date($stats['range_start']) . ' - ' . self::format_date($stats['range_end']); echo '<div class="fxdst-range-note"><strong>' . esc_html(self::zh('\u7edf\u8ba1\u8303\u56f4')) . '</strong> ' . esc_html($range); if (!empty($stats['first_event'])) { echo '<span>' . esc_html(self::zh('\u6709\u6570\u636e\u8d77\u59cb\uff1a') . self::format_date($stats['first_event'], true)) . '</span>'; if (strtotime($stats['first_event']) > strtotime($stats['range_start'])) { echo '<em>' . esc_html(self::zh('\u5f53\u524d\u7ad9\u70b9\u6ca1\u6709\u66f4\u65e9\u5386\u53f2\u6570\u636e\uff0c\u6240\u4ee5 7/30/90 \u5929\u7edf\u8ba1\u53ef\u80fd\u6682\u65f6\u76f8\u540c\u3002')) . '</em>'; } } else { echo '<span>' . esc_html(self::zh('\u6682\u65e0\u5386\u53f2\u6570\u636e')) . '</span>'; } echo '</div>'; } private static function admin_style() { static $printed = false; if ($printed) { return; } $printed = true; echo '<style>.fxdst-range-tabs{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:8px 0 12px}.fxdst-range-button{margin:0}.fxdst-range-current{color:#50575e;margin-left:4px}.fxdst-range-note{background:#fff;border-left:4px solid #2271b1;padding:10px 12px;margin:0 0 12px;display:flex;gap:12px;align-items:center;flex-wrap:wrap}.fxdst-range-note strong{color:#1d2327}.fxdst-range-note span{color:#50575e}.fxdst-range-note em{color:#8a6d3b;font-style:normal}.fxdst-stat-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:10px 0 16px}.fxdst-stat-card{background:#fff;border:1px solid #dcdcde;padding:12px}.fxdst-stat-card strong{display:block;font-size:22px;line-height:1.2}.fxdst-stat-tables,.fxdst-stat-two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;margin-top:16px}.fxdst-stat-table{background:#fff;border:1px solid #dcdcde;padding:12px}.postbox .fxdst-stat-table{border:0;padding:0}.fxdst-stat-table h3{margin:0 0 8px}.fxdst-stat-table table{width:100%;border-collapse:collapse}.fxdst-stat-table td,.fxdst-stat-table th{padding:6px;border-top:1px solid #f0f0f1;text-align:left;vertical-align:top}.fxdst-muted{color:#646970}.fxdst-url{max-width:460px;word-break:break-all}@media(max-width:1100px){.fxdst-stat-grid,.fxdst-stat-tables,.fxdst-stat-two{grid-template-columns:1fr}}#welcome-panel .welcome-panel-close{display:none!important}.fxdst-welcome-bar{display:flex;align-items:center;gap:18px;padding:18px 22px;background:linear-gradient(135deg,#101820,#18242f);color:#fff}.fxdst-welcome-title{min-width:240px}.fxdst-welcome-title strong{display:block;font-size:22px;line-height:1.2}.fxdst-welcome-title span{display:block;margin-top:6px;color:#b9c7d6}.fxdst-welcome-metrics{display:grid;grid-template-columns:repeat(5,minmax(110px,1fr));gap:10px;flex:1}.fxdst-welcome-metric{padding:12px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06)}.fxdst-welcome-metric span{display:block;color:#b9c7d6}.fxdst-welcome-metric strong{display:block;margin-top:4px;font-size:20px;line-height:1.2;word-break:break-all}.fxdst-welcome-metric em{display:block;margin-top:5px;color:#7ee0c0;font-style:normal}.fxdst-welcome-link{white-space:nowrap}@media(max-width:1400px){.fxdst-welcome-bar{display:block}.fxdst-welcome-metrics{margin:14px 0;grid-template-columns:repeat(3,minmax(0,1fr))}}@media(max-width:900px){.fxdst-welcome-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:700px){.fxdst-welcome-metrics{grid-template-columns:1fr}}</style>'; } private static function metric_card($label, $value, $hint) { echo '<div class="fxdst-stat-card"><span class="fxdst-muted">' . esc_html($label) . '</span><strong>' . esc_html(number_format_i18n((int)$value)) . '</strong><span class="fxdst-muted">' . esc_html($hint) . '</span></div>'; } private static function render_table($title, $rows, $headers, $type) { echo '<div class="fxdst-stat-table"><h3>' . esc_html($title) . '</h3><table><thead><tr>'; foreach ($headers as $h) { echo '<th>' . esc_html($h) . '</th>'; } echo '</tr></thead><tbody>'; if (!$rows) { echo '<tr><td colspan="' . esc_attr((string)count($headers)) . '" class="fxdst-muted">' . esc_html(self::zh('\u6682\u65e0\u6570\u636e')) . '</td></tr>'; } foreach ($rows as $row) { if ($type === 'source') { echo '<tr><td>' . esc_html(self::source_label($row->source)) . '</td><td>' . esc_html(self::medium_label($row->medium)) . '</td><td>' . esc_html($row->total) . '</td></tr>'; } elseif ($type === 'country') { echo '<tr><td>' . esc_html(self::country_label($row)) . '</td><td>' . esc_html($row->country_code ?: '-') . '</td><td>' . esc_html($row->total) . '</td></tr>'; } elseif ($type === 'keyword') { echo '<tr><td>' . esc_html($row->keyword) . '</td><td>' . esc_html($row->total) . '</td></tr>'; } elseif ($type === 'page') { echo '<tr><td class="fxdst-url"><a href="' . esc_url(home_url($row->page_path)) . '" target="_blank" rel="noopener">' . esc_html($row->page_path ?: '/') . '</a></td><td>' . esc_html($row->total) . '</td></tr>'; } else { echo '<tr><td class="fxdst-url"><a href="' . esc_url($row->target_url) . '" target="_blank" rel="noopener">' . esc_html(self::shorten($row->target_url, 80)) . '</a><br><span class="fxdst-muted">' . esc_html($row->target_text) . '</span></td><td>' . esc_html($row->total) . '</td></tr>'; } } echo '</tbody></table></div>'; } private static function render_recent($rows) { echo '<div class="fxdst-stat-table"><h3>' . esc_html(self::zh('\u6700\u8fd1\u4e8b\u4ef6')) . '</h3><table><thead><tr><th>' . esc_html(self::zh('\u65f6\u95f4')) . '</th><th>' . esc_html(self::zh('\u7c7b\u578b')) . '</th><th>' . esc_html(self::zh('\u9875\u9762 / \u70b9\u51fb')) . '</th><th>' . esc_html(self::zh('\u6765\u6e90')) . '</th><th>' . esc_html(self::zh('\u56fd\u5bb6/\u5730\u533a')) . '</th></tr></thead><tbody>'; if (!$rows) { echo '<tr><td colspan="5" class="fxdst-muted">' . esc_html(self::zh('\u6682\u65e0\u6570\u636e')) . '</td></tr>'; } foreach ($rows as $r) { $target = $r->event_type === 'click' ? $r->target_url : $r->page_path; echo '<tr><td>' . esc_html($r->event_time) . '</td><td>' . esc_html(self::event_type_label($r->event_type)) . '</td><td class="fxdst-url">' . esc_html(self::shorten($target, 90)) . '</td><td>' . esc_html(self::source_label($r->source)) . '</td><td>' . esc_html(self::country_label($r)) . '</td></tr>'; } echo '</tbody></table></div>'; } private static function get_stats($days) { global $wpdb; $table = self::table_name(); $now = new DateTimeImmutable('now', wp_timezone()); $today = $now->setTime(0, 0, 0)->format('Y-m-d H:i:s'); $since = $now->modify('-' . absint($days) . ' days')->format('Y-m-d H:i:s'); $range_end = $now->format('Y-m-d H:i:s'); $first_event = $wpdb->get_var("SELECT MIN(event_time) FROM {$table}"); return array( 'range_start' => $since, 'range_end' => $range_end, 'first_event' => $first_event, 'today_pageviews' => (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE event_type='pageview' AND event_time >= %s", $today)), 'today_visitors' => (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(DISTINCT visitor_hash) FROM {$table} WHERE event_type='pageview' AND event_time >= %s", $today)), 'period_pageviews' => (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE event_type='pageview' AND event_time >= %s", $since)), 'period_clicks' => (int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE event_type='click' AND event_time >= %s", $since)), 'sources' => $wpdb->get_results($wpdb->prepare("SELECT source, medium, COUNT(*) total FROM {$table} WHERE event_type='pageview' AND event_time >= %s GROUP BY source, medium ORDER BY total DESC LIMIT 10", $since)), 'countries' => $wpdb->get_results($wpdb->prepare("SELECT country_code, MAX(country_name) country_name, COUNT(*) total FROM {$table} WHERE event_type='pageview' AND country_code <> '' AND event_time >= %s GROUP BY country_code ORDER BY total DESC LIMIT 10", $since)), 'keywords' => $wpdb->get_results($wpdb->prepare("SELECT keyword, COUNT(*) total FROM {$table} WHERE keyword <> '' AND event_time >= %s GROUP BY keyword ORDER BY total DESC LIMIT 10", $since)), 'pages' => $wpdb->get_results($wpdb->prepare("SELECT page_path, COUNT(*) total FROM {$table} WHERE event_type='pageview' AND event_time >= %s GROUP BY page_path ORDER BY total DESC LIMIT 10", $since)), 'clicks' => $wpdb->get_results($wpdb->prepare("SELECT target_url, MAX(target_text) target_text, COUNT(*) total FROM {$table} WHERE event_type='click' AND target_url <> '' AND event_time >= %s GROUP BY target_url ORDER BY total DESC LIMIT 10", $since)), 'recent' => $wpdb->get_results($wpdb->prepare("SELECT event_time, event_type, page_path, target_url, source, country_code, country_name FROM {$table} WHERE event_time >= %s ORDER BY id DESC LIMIT 20", $since)), ); } private static function format_date($mysql, $with_time = false) { $timestamp = strtotime((string)$mysql); if (!$timestamp) { return '-'; } return date_i18n($with_time ? 'Y-m-d H:i' : 'Y-m-d', $timestamp); } private static function classify_referrer($referrer, $utm) { if (!empty($utm['utm_source'])) { return array('source' => self::clean_text($utm['utm_source'], 80), 'medium' => self::clean_text($utm['utm_medium'] ?? 'campaign', 80)); } if (!$referrer) { return array('source' => 'direct', 'medium' => 'direct'); } $host = strtolower(wp_parse_url($referrer, PHP_URL_HOST) ?: ''); $site_host = strtolower(wp_parse_url(home_url('/'), PHP_URL_HOST) ?: ''); if ($host === $site_host || self::ends_with($host, '.' . $site_host)) { return array('source' => 'internal', 'medium' => 'internal'); } foreach (array('google.', 'baidu.', 'bing.', 'sogou.', 'so.com', 'sm.cn', 'yahoo.', 'duckduckgo.') as $needle) { if (strpos($host, $needle) !== false) { return array('source' => $host, 'medium' => 'organic'); } } return array('source' => $host ?: 'referral', 'medium' => 'referral'); } private static function extract_keyword($url) { $params = self::parse_query_params($url); foreach (array('q', 'wd', 'word', 'query', 'keyword', 'text', 'p') as $key) { if (!empty($params[$key])) { return self::clean_text($params[$key], 191); } } return ''; } private static function parse_query_params($url) { $query = wp_parse_url($url, PHP_URL_QUERY); if (!$query) { return array(); } $params = array(); parse_str($query, $params); return is_array($params) ? $params : array(); } private static function parse_url_parts($url) { if (!$url) { return array('path' => '/', 'host' => '', 'query' => array()); } return array( 'host' => strtolower(wp_parse_url($url, PHP_URL_HOST) ?: ''), 'path' => wp_parse_url($url, PHP_URL_PATH) ?: '/', 'query' => self::parse_query_params($url), ); } private static function clean_url($value) { $value = is_string($value) ? trim(wp_unslash($value)) : ''; return esc_url_raw(substr($value, 0, 2000)); } private static function tracked_post_id() { $post_id = absint($_POST['post_id'] ?? 0); if ($post_id && in_array(get_post_type($post_id), array('post', 'sites'), true)) { return $post_id; } return get_queried_object_id() ?: 0; } private static function post_pageviews($post_id) { static $cache = array(); $post_id = absint($post_id); if (!$post_id) { return 0; } if (isset($cache[$post_id])) { return $cache[$post_id]; } if (!self::analytics_table_exists()) { $cache[$post_id] = 0; return 0; } global $wpdb; $paths = self::post_public_paths($post_id); $where = array('post_id = %d'); $args = array($post_id); if ($paths) { $where[] = 'page_path IN (' . implode(',', array_fill(0, count($paths), '%s')) . ')'; $args = array_merge($args, $paths); } $sql = "SELECT COUNT(*) FROM " . self::table_name() . " WHERE event_type = 'pageview' AND (" . implode(' OR ', $where) . ")"; $prepared = call_user_func_array(array($wpdb, 'prepare'), array_merge(array($sql), $args)); $cache[$post_id] = (int)$wpdb->get_var($prepared); return $cache[$post_id]; } private static function analytics_table_exists() { static $exists = null; if ($exists !== null) { return $exists; } global $wpdb; $table = self::table_name(); $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) === $table; return $exists; } private static function post_public_paths($post_id) { $url = get_permalink($post_id); if (!$url) { return array(); } $path = wp_parse_url($url, PHP_URL_PATH); if (!$path) { return array(); } $path = '/' . ltrim($path, '/'); $paths = array($path); $trimmed = rtrim($path, '/'); if ($trimmed !== '' && $trimmed !== $path) { $paths[] = $trimmed; } $trailed = trailingslashit($trimmed ?: $path); if ($trailed !== $path) { $paths[] = $trailed; } return array_values(array_unique($paths)); } private static function clean_text($value, $limit) { $value = is_string($value) ? wp_unslash($value) : ''; $value = sanitize_text_field($value); return function_exists('mb_substr') ? mb_substr($value, 0, $limit, 'UTF-8') : substr($value, 0, $limit); } private static function clean_key($value) { return sanitize_key(is_string($value) ? wp_unslash($value) : ''); } private static function hash_value($value) { return hash_hmac('sha256', (string)$value, wp_salt('auth')); } private static function client_ip() { foreach (array('HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR') as $key) { if (!empty($_SERVER[$key])) { return trim(explode(',', (string)$_SERVER[$key])[0]); } } return ''; } private static function detect_country($ip) { $code = ''; foreach (array('HTTP_CF_IPCOUNTRY', 'HTTP_CLOUDFRONT_VIEWER_COUNTRY', 'HTTP_X_COUNTRY_CODE', 'HTTP_X_GEOIP_COUNTRY_CODE', 'GEOIP_COUNTRY_CODE') as $key) { if (!empty($_SERVER[$key])) { $code = self::clean_country_code($_SERVER[$key]); if ($code !== '') { break; } } } if ($code === '' && $ip && function_exists('geoip_country_code_by_name')) { $code = self::clean_country_code(@geoip_country_code_by_name($ip)); } if ($code === '' && $ip) { $code = self::country_code_from_maxmind_reader($ip); } if ($code === '' && $ip) { $code = self::country_code_from_mmdb_command($ip); } return array( 'code' => $code, 'name' => $code ? self::country_name($code) : '', ); } private static function country_code_from_maxmind_reader($ip) { if (!filter_var($ip, FILTER_VALIDATE_IP)) { return ''; } $autoload = plugin_dir_path(__FILE__) . 'vendor/autoload.php'; $db = self::mmdb_path(); if ($db === '' || !is_readable($autoload)) { return ''; } if (!class_exists('MaxMind\\Db\\Reader')) { require_once $autoload; } if (!class_exists('MaxMind\\Db\\Reader')) { return ''; } try { $reader = new MaxMind\Db\Reader($db); $data = $reader->get($ip); $reader->close(); return self::clean_country_code($data['country']['iso_code'] ?? ''); } catch (Exception $e) { return ''; } } private static function country_code_from_mmdb_command($ip) { if (!filter_var($ip, FILTER_VALIDATE_IP) || !function_exists('shell_exec')) { return ''; } $cache_key = 'fxdst_country_' . substr(self::hash_value($ip), 0, 24); $cached = get_transient($cache_key); if (is_string($cached)) { return self::clean_country_code($cached); } $db = self::mmdb_path(); if ($db === '') { return ''; } $cmd = 'mmdblookup --file ' . escapeshellarg($db) . ' --ip ' . escapeshellarg($ip) . ' country iso_code 2>/dev/null'; $output = shell_exec($cmd); if (!is_string($output) || $output === '') { return ''; } if (!preg_match('/"([A-Za-z]{2})"/', $output, $m)) { return ''; } $code = self::clean_country_code($m[1]); if ($code !== '') { set_transient($cache_key, $code, WEEK_IN_SECONDS); } return $code; } private static function mmdb_path() { foreach (array('/usr/share/GeoIP/GeoLite2-Country.mmdb', '/usr/share/GeoIP/GeoLite2-City.mmdb', '/www/server/panel/config/GeoLite2-City.mmdb') as $path) { if (is_readable($path)) { return $path; } } return ''; } private static function clean_country_code($value) { $code = strtoupper(substr(preg_replace('/[^A-Za-z]/', '', (string)$value), 0, 2)); return preg_match('/^[A-Z]{2}$/', $code) && !in_array($code, array('XX', 'T1'), true) ? $code : ''; } private static function country_name($code) { $code = self::clean_country_code($code); if ($code === '') { return ''; } if (class_exists('Locale')) { $name = Locale::getDisplayRegion('und_' . $code, 'zh_CN'); if (is_string($name) && $name !== '') { return $name; } } return $code; } private static function country_label($row) { $name = isset($row->country_name) ? (string)$row->country_name : ''; $code = isset($row->country_code) ? (string)$row->country_code : ''; if ($name !== '') { return $name; } if ($code !== '') { return self::country_name($code); } return '-'; } private static function source_label($source) { $source = trim((string)$source); $lower = strtolower($source); $map = array( '' => self::zh('\u672a\u77e5\u6765\u6e90'), 'direct' => self::zh('\u76f4\u63a5\u8bbf\u95ee'), 'internal' => self::zh('\u7ad9\u5185\u8bbf\u95ee'), 'referral' => self::zh('\u5916\u90e8\u5f15\u7528'), 'testsource' => self::zh('\u6d4b\u8bd5\u6765\u6e90'), 'codexcountry' => self::zh('\u6d4b\u8bd5\u6765\u6e90'), ); if (isset($map[$lower])) { return $map[$lower]; } if (strpos($lower, 'google.') !== false) { return 'Google ' . self::zh('\u641c\u7d22'); } if (strpos($lower, 'baidu.') !== false) { return self::zh('\u767e\u5ea6\u641c\u7d22'); } if (strpos($lower, 'bing.') !== false) { return 'Bing ' . self::zh('\u641c\u7d22'); } if (strpos($lower, 'sogou.') !== false) { return self::zh('\u641c\u72d7\u641c\u7d22'); } if (strpos($lower, 'duckduckgo.') !== false) { return 'DuckDuckGo ' . self::zh('\u641c\u7d22'); } return self::zh('\u81ea\u5b9a\u4e49\u6765\u6e90\uff1a') . $source; } private static function medium_label($medium) { $medium = trim((string)$medium); $lower = strtolower($medium); $map = array( '' => '-', 'direct' => self::zh('\u76f4\u63a5'), 'internal' => self::zh('\u7ad9\u5185'), 'organic' => self::zh('\u81ea\u7136\u641c\u7d22'), 'referral' => self::zh('\u5f15\u7528'), 'campaign' => self::zh('\u63a8\u5e7f\u6d3b\u52a8'), 'test' => self::zh('\u6d4b\u8bd5'), 'email' => self::zh('\u90ae\u4ef6'), 'social' => self::zh('\u793e\u4ea4'), 'cpc' => self::zh('\u4ed8\u8d39\u70b9\u51fb'), 'paid' => self::zh('\u4ed8\u8d39\u63a8\u5e7f'), ); return $map[$lower] ?? (self::zh('\u81ea\u5b9a\u4e49\u5a92\u4ecb\uff1a') . $medium); } private static function event_type_label($event_type) { $map = array( 'pageview' => self::zh('\u9875\u9762\u8bbf\u95ee'), 'click' => self::zh('\u70b9\u51fb'), 'search' => self::zh('\u7ad9\u5185\u641c\u7d22'), ); return $map[(string)$event_type] ?? (string)$event_type; } private static function detect_device($ua) { $ua = strtolower((string)$ua); if (strpos($ua, 'mobile') !== false || strpos($ua, 'android') !== false || strpos($ua, 'iphone') !== false) return 'mobile'; if (strpos($ua, 'ipad') !== false || strpos($ua, 'tablet') !== false) return 'tablet'; return 'desktop'; } private static function detect_browser($ua) { $ua = strtolower((string)$ua); if (strpos($ua, 'edg/') !== false) return 'Edge'; if (strpos($ua, 'chrome/') !== false) return 'Chrome'; if (strpos($ua, 'safari/') !== false && strpos($ua, 'chrome/') === false) return 'Safari'; if (strpos($ua, 'firefox/') !== false) return 'Firefox'; if (strpos($ua, 'micromessenger') !== false) return 'WeChat'; return 'Other'; } private static function is_bot($ua) { $ua = trim((string)$ua); if ($ua === '' || preg_match('/^(pc|mobile)$/i', $ua)) { return true; } return (bool)preg_match('/bot|spider|crawl|crawler|slurp|baiduspider|bingpreview|bytespider|headless|lighthouse|curl|wget|python|scrapy|httpclient/i', $ua); } private static function shorten($text, $length) { $text = (string)$text; if (function_exists('mb_strlen') && mb_strlen($text, 'UTF-8') > $length) return mb_substr($text, 0, $length, 'UTF-8') . '...'; return strlen($text) > $length ? substr($text, 0, $length) . '...' : $text; } private static function ends_with($haystack, $needle) { return $needle === '' || substr($haystack, -strlen($needle)) === $needle; } } register_activation_hook(__FILE__, array('FXDST_Analytics', 'maybe_install')); FXDST_Analytics::boot();