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:
@@ -9,7 +9,7 @@ class RequestLogger
|
||||
$this->logFile = $logFile;
|
||||
}
|
||||
|
||||
public function log(string $page, string $ip, string $userAgent, string $referrer, string $host, string $acceptLanguage, string $status = 'ok'): void
|
||||
public function log(string $page, string $ip, string $userAgent, string $referrer, string $host, string $acceptLanguage, string $status = 'ok', ?string $country = null): void
|
||||
{
|
||||
$dir = dirname($this->logFile);
|
||||
if (!is_dir($dir)) {
|
||||
@@ -19,10 +19,31 @@ class RequestLogger
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$ua = substr(preg_replace('/[[:cntrl:]]/', '', $userAgent), 0, 500);
|
||||
$ref = substr(preg_replace('/[[:cntrl:]]/', '', $referrer), 0, 500);
|
||||
$line = "[{$timestamp}] [{$ip}] [{$host}] [{$acceptLanguage}] [{$page}] [{$ua}] [{$ref}] [{$status}]\n";
|
||||
$cc = ($country && strlen($country) === 2) ? strtoupper($country) : '';
|
||||
$line = "[{$timestamp}] [{$ip}] [{$host}] [{$acceptLanguage}] [{$page}] [{$ua}] [{$ref}] [{$status}] [{$cc}]\n";
|
||||
@file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask the last octet (IPv4) or last block (IPv6) of an IP address
|
||||
*/
|
||||
public static function anonymizeIp(string $ip): string
|
||||
{
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$parts = explode('.', $ip);
|
||||
if (count($parts) === 4) {
|
||||
$parts[3] = 'x';
|
||||
return implode('.', $parts);
|
||||
}
|
||||
}
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||
$parts = explode(':', $ip);
|
||||
$keep = array_slice($parts, 0, 4);
|
||||
return implode(':', $keep) . '::x';
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
|
||||
public static function getClientIp(): string
|
||||
{
|
||||
$headerKeys = [
|
||||
@@ -132,13 +153,14 @@ class RequestLogger
|
||||
|
||||
foreach ($content as $line) {
|
||||
$trimmed = trim($line);
|
||||
if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]*)\](?: \[([^\]]*)\])?$/', $trimmed, $m)) {
|
||||
if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]*)\](?: \[([^\]]*)\])?(?: \[([^\]]*)\])?$/', $trimmed, $m)) {
|
||||
$user = $m[3];
|
||||
if (str_contains($user, '.') || str_contains($user, ':') || $user === 'cli') {
|
||||
$user = 'Gast';
|
||||
}
|
||||
$status = $m[8] ?? 'ok';
|
||||
if ($status === '') $status = 'ok';
|
||||
$country = $m[9] ?? '';
|
||||
|
||||
$visitorInfo = self::detectVisitorInfo($m[6], $user);
|
||||
$logs[] = [
|
||||
@@ -151,6 +173,7 @@ class RequestLogger
|
||||
'ua' => $m[6],
|
||||
'referrer' => $m[7],
|
||||
'status' => $status,
|
||||
'country' => $country,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user