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:
2026-07-29 15:40:02 +02:00
parent 8c6b38c2c5
commit 06785e9922
20 changed files with 1804 additions and 11 deletions
+97 -1
View File
@@ -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);