CodePress CMS v1.9.0: visitor statistics with SVG world map and GeoIP
- Add GeoIP class with provider chain: local DB-IP Lite, MaxMind .mmdb, external API - Add built-in pure-PHP MMDBReader so .mmdb works without Composer - Add cli/geoip-update.php to download DB-IP Lite and build a compact binary index - Add cli/generate-world-map.php to generate the world map SVG from Natural Earth TopoJSON - Add Analytics class aggregating stats in admin/storage/stats.json with LOCK_EX - Add admin statistics page with choropleth world map, country list, top pages, daily chart, referrers and a period filter - Add GeoIP and privacy settings with database update and stats reset buttons - Add optional IP anonymization and configurable retention period - Add country field to requests.log (parser accepts 7, 8 or 9 fields) - Add country column to request log and KPI cards to the dashboard - Ignore GeoIP binaries and stats.json in Git - Update TODO.md, guides and version to 1.9.0
This commit is contained in:
+97
-1
@@ -22,6 +22,10 @@ if (file_exists($autoloader)) {
|
||||
$appConfig = require __DIR__ . '/../admin/config/app.php';
|
||||
require_once __DIR__ . '/../admin/src/AdminAuth.php';
|
||||
require_once __DIR__ . '/../cms/core/class/RequestLogger.php';
|
||||
require_once __DIR__ . '/../cms/core/class/BotGuard.php';
|
||||
require_once __DIR__ . '/../cms/core/class/Cache.php';
|
||||
require_once __DIR__ . '/../cms/core/class/GeoIP.php';
|
||||
require_once __DIR__ . '/../cms/core/class/Analytics.php';
|
||||
|
||||
$auth = new AdminAuth($appConfig);
|
||||
|
||||
@@ -92,6 +96,10 @@ switch ($route) {
|
||||
handleSecurity($auth, $appConfig);
|
||||
break;
|
||||
|
||||
case 'statistics':
|
||||
handleStatistics($auth, $appConfig);
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
handleUpdate($auth, $appConfig);
|
||||
break;
|
||||
@@ -232,6 +240,11 @@ function handleDashboard(AdminAuth $auth, array $config): void
|
||||
$requestLogger = new RequestLogger($requestLogFile);
|
||||
$recentRequests = $requestLogger->getLogs(20);
|
||||
|
||||
// Analytics summary (last 30 days)
|
||||
$siteAnalytics = is_array($siteConfig['analytics'] ?? null) ? $siteConfig['analytics'] : [];
|
||||
$analytics = new Analytics($siteAnalytics);
|
||||
$analyticsSummary = $analytics->getStats(30);
|
||||
|
||||
require __DIR__ . '/../admin/templates/layout.php';
|
||||
}
|
||||
|
||||
@@ -809,6 +822,90 @@ function handleSecurity(AdminAuth $auth, array $config): void
|
||||
require __DIR__ . '/../admin/templates/layout.php';
|
||||
}
|
||||
|
||||
function handleStatistics(AdminAuth $auth, array $config): void
|
||||
{
|
||||
$user = $auth->getCurrentUser();
|
||||
$csrf = $auth->getCsrfToken();
|
||||
$configJson = $config['config_json'];
|
||||
$message = '';
|
||||
$messageType = '';
|
||||
|
||||
require_once __DIR__ . '/../cms/core/class/Cache.php';
|
||||
require_once __DIR__ . '/../cms/core/class/GeoIP.php';
|
||||
require_once __DIR__ . '/../cms/core/class/Analytics.php';
|
||||
require_once __DIR__ . '/../cms/core/class/BotGuard.php';
|
||||
|
||||
$configData = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
||||
if (!is_array($configData)) $configData = [];
|
||||
|
||||
$analyticsDefaults = [
|
||||
'enabled' => true,
|
||||
'anonymize_ip' => false,
|
||||
'geoip_provider' => 'local',
|
||||
'geoip_mmdb_path' => '',
|
||||
'geoip_api_url' => '',
|
||||
'geoip_api_key' => '',
|
||||
'retention_days' => 400,
|
||||
];
|
||||
$ana = array_merge($analyticsDefaults, is_array($configData['analytics'] ?? null) ? $configData['analytics'] : []);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
||||
$message = 'Ongeldige CSRF token.';
|
||||
$messageType = 'danger';
|
||||
} elseif (($_POST['action'] ?? '') === 'update_geoip') {
|
||||
require_once __DIR__ . '/../cli/geoip-update.php';
|
||||
$result = updateGeoIPDatabase();
|
||||
$message = $result['message'];
|
||||
$messageType = $result['success'] ? 'success' : 'danger';
|
||||
adminLog($config, 'info', $user['username'] . ' werkte de GeoIP database bij');
|
||||
} elseif (($_POST['action'] ?? '') === 'reset_stats') {
|
||||
$statsPath = $config['codepress_root'] . '/admin/storage/stats.json';
|
||||
@unlink($statsPath);
|
||||
adminLog($config, 'warning', $user['username'] . ' wiste alle statistieken');
|
||||
$message = 'Alle statistieken zijn gewist.';
|
||||
$messageType = 'success';
|
||||
} else {
|
||||
$configData['analytics']['enabled'] = !empty($_POST['analytics_enabled']);
|
||||
$configData['analytics']['anonymize_ip'] = !empty($_POST['anonymize_ip']);
|
||||
$provider = $_POST['geoip_provider'] ?? 'local';
|
||||
$configData['analytics']['geoip_provider'] = in_array($provider, ['local', 'mmdb', 'api'], true) ? $provider : 'local';
|
||||
$configData['analytics']['geoip_mmdb_path'] = trim($_POST['geoip_mmdb_path'] ?? '');
|
||||
$configData['analytics']['geoip_api_url'] = trim($_POST['geoip_api_url'] ?? '');
|
||||
$configData['analytics']['geoip_api_key'] = trim($_POST['geoip_api_key'] ?? '');
|
||||
$configData['analytics']['retention_days'] = max(30, min(3650, (int)($_POST['retention_days'] ?? 400)));
|
||||
|
||||
file_put_contents($configJson, json_encode($configData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
adminLog($config, 'info', $user['username'] . ' wijzigde statistiek-instellingen');
|
||||
$message = 'Statistiek-instellingen opgeslagen.';
|
||||
$messageType = 'success';
|
||||
$ana = array_merge($analyticsDefaults, $configData['analytics']);
|
||||
}
|
||||
}
|
||||
|
||||
// Period filter
|
||||
$period = (int)($_GET['period'] ?? 30);
|
||||
if (!in_array($period, [7, 30, 90, 0], true)) $period = 30;
|
||||
|
||||
$analytics = new Analytics($ana);
|
||||
$stats = $analytics->getStats($period);
|
||||
|
||||
// GeoIP database status
|
||||
$geoDir = $config['codepress_root'] . '/admin/storage/geoip';
|
||||
$geoMeta = null;
|
||||
if (file_exists($geoDir . '/meta.json')) {
|
||||
$geoMeta = json_decode(file_get_contents($geoDir . '/meta.json'), true);
|
||||
}
|
||||
|
||||
// World map SVG
|
||||
$worldMapPath = $config['codepress_root'] . '/public/assets/img/world-map.svg';
|
||||
$worldMapSvg = file_exists($worldMapPath) ? file_get_contents($worldMapPath) : '';
|
||||
$worldMapSvg = preg_replace('/^<\?xml[^>]*\?>\s*/', '', $worldMapSvg);
|
||||
|
||||
$route = 'statistics';
|
||||
require __DIR__ . '/../admin/templates/layout.php';
|
||||
}
|
||||
|
||||
function handleTheme(AdminAuth $auth, array $config): void
|
||||
{
|
||||
$user = $auth->getCurrentUser();
|
||||
@@ -1576,7 +1673,6 @@ function handleLogs(AdminAuth $auth, array $config): void
|
||||
}
|
||||
|
||||
// Read request log
|
||||
require_once __DIR__ . '/../cms/core/class/RequestLogger.php';
|
||||
$requestLogger = new RequestLogger($requestLogFile);
|
||||
$requestLogs = $requestLogger->getLogs(200);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user