192 lines
5.3 KiB
PHP
192 lines
5.3 KiB
PHP
<?php
|
|
namespace AuroraAdmin\Analytics;
|
|
|
|
defined("ABSPATH") || exit();
|
|
|
|
/**
|
|
* Lightweight first-party visitor analytics.
|
|
*
|
|
* Records one row per front-end pageview into a custom table, server-side
|
|
* (no client script, so it isn't blocked by ad blockers). All aggregation
|
|
* happens later in the REST endpoint. Written fresh — does not reuse the
|
|
* original plugin's analytics code.
|
|
*/
|
|
class Analytics
|
|
{
|
|
const TABLE = "aurora_admin_pageviews";
|
|
const DB_VERSION = "1";
|
|
const DB_OPTION = "aurora_admin_analytics_db_version";
|
|
const COOKIE = "aurora_admin_sid";
|
|
|
|
public function __construct()
|
|
{
|
|
add_action("init", [self::class, "maybe_install"]);
|
|
// Record on the front end only, after the main query is resolved.
|
|
add_action("template_redirect", [self::class, "record"], 999);
|
|
}
|
|
|
|
public static function table_name()
|
|
{
|
|
global $wpdb;
|
|
return $wpdb->prefix . self::TABLE;
|
|
}
|
|
|
|
public static function maybe_install()
|
|
{
|
|
if (get_option(self::DB_OPTION) === self::DB_VERSION) {
|
|
return;
|
|
}
|
|
|
|
global $wpdb;
|
|
$table = self::table_name();
|
|
$charset = $wpdb->get_charset_collate();
|
|
|
|
$sql = "CREATE TABLE {$table} (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
session_id VARCHAR(32) NOT NULL DEFAULT '',
|
|
url VARCHAR(255) NOT NULL DEFAULT '',
|
|
referrer VARCHAR(255) NOT NULL DEFAULT '',
|
|
device VARCHAR(20) NOT NULL DEFAULT 'desktop',
|
|
browser VARCHAR(40) NOT NULL DEFAULT '',
|
|
country VARCHAR(2) NOT NULL DEFAULT '',
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id),
|
|
KEY idx_created (created_at),
|
|
KEY idx_session (session_id)
|
|
) {$charset};";
|
|
|
|
require_once ABSPATH . "wp-admin/includes/upgrade.php";
|
|
dbDelta($sql);
|
|
|
|
update_option(self::DB_OPTION, self::DB_VERSION);
|
|
}
|
|
|
|
/**
|
|
* Records the current front-end request as a pageview, skipping admin,
|
|
* AJAX/REST/cron, bots, and logged-in administrators.
|
|
*/
|
|
public static function record()
|
|
{
|
|
// Tracking is on unless explicitly disabled in Settings > General.
|
|
$enabled = \AuroraAdmin\Options\Settings::get("enable_analytics", true);
|
|
if ($enabled === false || $enabled === 0 || $enabled === "0") {
|
|
return;
|
|
}
|
|
|
|
if (is_admin() || wp_doing_ajax() || wp_doing_cron() || is_robots() || is_feed()) {
|
|
return;
|
|
}
|
|
if (defined("REST_REQUEST") && REST_REQUEST) {
|
|
return;
|
|
}
|
|
|
|
// Admins browsing their own site are skipped unless Settings >
|
|
// Analytics > "Track logged-in admins" is enabled.
|
|
$track_admins = \AuroraAdmin\Options\Settings::get("track_admins", false);
|
|
if (!$track_admins && current_user_can("manage_options")) {
|
|
return;
|
|
}
|
|
|
|
$ua = isset($_SERVER["HTTP_USER_AGENT"]) ? $_SERVER["HTTP_USER_AGENT"] : "";
|
|
if (self::is_bot($ua)) {
|
|
return;
|
|
}
|
|
|
|
global $wpdb;
|
|
$wpdb->insert(self::table_name(), [
|
|
"session_id" => self::session_id(),
|
|
"url" => substr(self::current_path(), 0, 255),
|
|
"referrer" => substr(self::referrer(), 0, 255),
|
|
"device" => self::device($ua),
|
|
"browser" => self::browser($ua),
|
|
"country" => self::country(),
|
|
"created_at" => current_time("mysql"),
|
|
]);
|
|
}
|
|
|
|
private static function session_id()
|
|
{
|
|
if (!empty($_COOKIE[self::COOKIE])) {
|
|
return preg_replace("/[^a-f0-9]/", "", substr($_COOKIE[self::COOKIE], 0, 32));
|
|
}
|
|
$sid = md5(uniqid("", true));
|
|
// 30-minute rolling session window
|
|
if (!headers_sent()) {
|
|
setcookie(self::COOKIE, $sid, time() + 1800, COOKIEPATH ?: "/", COOKIE_DOMAIN);
|
|
}
|
|
$_COOKIE[self::COOKIE] = $sid;
|
|
return $sid;
|
|
}
|
|
|
|
private static function current_path()
|
|
{
|
|
$uri = isset($_SERVER["REQUEST_URI"]) ? $_SERVER["REQUEST_URI"] : "/";
|
|
return wp_parse_url($uri, PHP_URL_PATH) ?: "/";
|
|
}
|
|
|
|
private static function referrer()
|
|
{
|
|
$ref = isset($_SERVER["HTTP_REFERER"]) ? $_SERVER["HTTP_REFERER"] : "";
|
|
if (!$ref) {
|
|
return "";
|
|
}
|
|
$host = wp_parse_url($ref, PHP_URL_HOST);
|
|
// Ignore self-referrals
|
|
if ($host && $host === wp_parse_url(home_url(), PHP_URL_HOST)) {
|
|
return "";
|
|
}
|
|
return $host ?: "";
|
|
}
|
|
|
|
private static function device($ua)
|
|
{
|
|
$ua = strtolower($ua);
|
|
if (preg_match("/(ipad|tablet|playbook|silk)|(android(?!.*mobile))/", $ua)) {
|
|
return "tablet";
|
|
}
|
|
if (preg_match("/(mobile|iphone|ipod|android|blackberry|opera mini|iemobile)/", $ua)) {
|
|
return "mobile";
|
|
}
|
|
return "desktop";
|
|
}
|
|
|
|
private static function browser($ua)
|
|
{
|
|
$map = [
|
|
"Edg" => "Edge",
|
|
"OPR" => "Opera",
|
|
"Chrome" => "Chrome",
|
|
"Firefox" => "Firefox",
|
|
"Safari" => "Safari",
|
|
];
|
|
foreach ($map as $needle => $name) {
|
|
if (stripos($ua, $needle) !== false) {
|
|
return $name;
|
|
}
|
|
}
|
|
return "Other";
|
|
}
|
|
|
|
private static function country()
|
|
{
|
|
// Use a CDN-provided country header if available; otherwise leave blank.
|
|
foreach (["HTTP_CF_IPCOUNTRY", "GEOIP_COUNTRY_CODE", "HTTP_X_COUNTRY_CODE"] as $h) {
|
|
if (!empty($_SERVER[$h]) && strlen($_SERVER[$h]) === 2) {
|
|
return strtoupper(sanitize_text_field($_SERVER[$h]));
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
private static function is_bot($ua)
|
|
{
|
|
if (!$ua) {
|
|
return true;
|
|
}
|
|
return (bool) preg_match(
|
|
"/bot|crawl|slurp|spider|mediapartners|facebookexternalhit|embedly|quora|pingdom|monitor|headless|lighthouse|gtmetrix/i",
|
|
$ua
|
|
);
|
|
}
|
|
}
|