Analytics configuratie. */ private array $config; /** * Initialiseer de Analytics-recorder. * * Stelt het pad naar het statistiekbestand in en maakt de opslagmap aan * indien deze nog niet bestaat. * * @since 2.6.5 * * @param array $analyticsConfig { * Optioneel. Analytics configuratie. * * @type bool $enabled Of analytics actief is. Default false. * @type int $retention_days Aantal dagen dat statistieken bewaard blijven. Default 400. * } */ public function __construct(array $analyticsConfig = []) { $this->config = $analyticsConfig; $this->statsFile = dirname(__DIR__, 3) . '/admin/storage/stats.json'; $dir = dirname($this->statsFile); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } } /** * Registreer een paginabezoek in de geaggregeerde statistieken. * * Identificeert bezoeker (bot/mens), anonimiseert het IP-adres per dag met * een zout, aggregeert totals/countries/pages/referrers/days/uniques, past * de retentie toe en schrijft atomair (met bestandslock) naar stats.json. * * @since 2.6.5 * * @param string $page Page key van de bezochte pagina. * @param string $ip Client IP-adres. * @param string $userAgent User-Agent string van de bezoeker. * @param string $referrer Referrer URL string. * @param string|null $country Opgeloste landcode (bijv. NL, BE). Null indien onbekend. * @param string $status Status string (ok, blocked:ai, blocked:ratelimit, enz.). * @return void */ public function record(string $page, string $ip, string $userAgent, string $referrer, ?string $country, string $status): void { if (empty($this->config['enabled'])) { return; } $today = date('Y-m-d'); $country = ($country && strlen($country) === 2) ? strtoupper($country) : 'UNKNOWN'; $isBlocked = str_starts_with($status, 'blocked'); // Identify bot vs human $visitorInfo = RequestLogger::detectVisitorInfo($userAgent); $type = $visitorInfo['type']; $isBot = in_array($type, ['ai', 'search', 'scraper', 'bot'], true); // Anonymized unique IP salt per day $ipSalt = date('Y-m-d') . '_codepress_salt'; $ipHash = md5($ip . $ipSalt); // Domain/host from referrer $refHost = 'direct'; if ($referrer !== '') { $parsed = parse_url($referrer); if (!empty($parsed['host'])) { $refHost = preg_replace('/^www\./', '', strtolower($parsed['host'])); } } // Open stats.json with exclusive lock $handle = @fopen($this->statsFile, 'c+'); if (!$handle) return; if (flock($handle, LOCK_EX)) { $fileSize = filesize($this->statsFile); $data = []; if ($fileSize > 0) { $content = fread($handle, $fileSize); $data = json_decode($content, true); } if (!is_array($data)) { $data = [ 'totals' => ['views' => 0, 'human' => 0, 'bot' => 0, 'blocked' => 0], 'countries' => [], 'pages' => [], 'referrers' => [], 'days' => [], 'uniques' => [] ]; } // Totals $data['totals']['views'] = ($data['totals']['views'] ?? 0) + 1; if ($isBlocked) { $data['totals']['blocked'] = ($data['totals']['blocked'] ?? 0) + 1; } elseif ($isBot) { $data['totals']['bot'] = ($data['totals']['bot'] ?? 0) + 1; } else { $data['totals']['human'] = ($data['totals']['human'] ?? 0) + 1; } // Countries $data['countries'][$country] = ($data['countries'][$country] ?? 0) + 1; // Pages $data['pages'][$page] = ($data['pages'][$page] ?? 0) + 1; // Referrers if ($refHost !== 'direct') { $data['referrers'][$refHost] = ($data['referrers'][$refHost] ?? 0) + 1; } // Day stats if (!isset($data['days'][$today])) { $data['days'][$today] = ['views' => 0, 'human' => 0, 'bot' => 0, 'blocked' => 0, 'countries' => [], 'pages' => []]; } $data['days'][$today]['views']++; if ($isBlocked) { $data['days'][$today]['blocked']++; } elseif ($isBot) { $data['days'][$today]['bot']++; } else { $data['days'][$today]['human']++; } $data['days'][$today]['countries'][$country] = ($data['days'][$today]['countries'][$country] ?? 0) + 1; $data['days'][$today]['pages'][$page] = ($data['days'][$today]['pages'][$page] ?? 0) + 1; // Uniques per day if (!isset($data['uniques'][$today])) { $data['uniques'][$today] = []; } if (!in_array($ipHash, $data['uniques'][$today], true)) { $data['uniques'][$today][] = $ipHash; } // Cleanup retention (keep max retention_days) $retentionDays = max(30, (int)($this->config['retention_days'] ?? 400)); $cutoffDate = date('Y-m-d', strtotime("-{$retentionDays} days")); foreach (array_keys($data['days']) as $d) { if ($d < $cutoffDate) { unset($data['days'][$d]); unset($data['uniques'][$d]); } } // Rewrite stats file ftruncate($handle, 0); rewind($handle); fwrite($handle, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); fflush($handle); flock($handle, LOCK_UN); } fclose($handle); } /** * Haal geaggregeerde statistieken op voor een specifieke periode. * * Leest stats.json en aggregeert totals, landen, pagina's, referrers en * een daily chart. Bij $days = 0 wordt alles-tijd geretourneerd, anders * worden ontbrekende dagen in de range opgevuld voor een vloeiende grafiek. * * @since 2.6.5 * * @param int $days Aantal dagen (bijv. 7, 30, 90), of 0 voor alles-tijd. Default 30. * @return array Geaggregeerde statistieken met keys totals, countries, pages, referrers, daily_chart. */ public function getStats(int $days = 30): array { if (!file_exists($this->statsFile)) { return [ 'totals' => ['views' => 0, 'human' => 0, 'bot' => 0, 'blocked' => 0, 'uniques' => 0], 'countries' => [], 'pages' => [], 'referrers' => [], 'daily_chart' => [], ]; } $raw = json_decode(file_get_contents($this->statsFile), true); if (!is_array($raw)) $raw = []; if ($days === 0) { // All time $countries = $raw['countries'] ?? []; $pages = $raw['pages'] ?? []; $referrers = $raw['referrers'] ?? []; $totals = $raw['totals'] ?? ['views' => 0, 'human' => 0, 'bot' => 0, 'blocked' => 0]; $totalUniques = 0; foreach ($raw['uniques'] ?? [] as $uList) { $totalUniques += count($uList); } $totals['uniques'] = $totalUniques; $dailyChart = []; foreach ($raw['days'] ?? [] as $date => $dData) { $dailyChart[$date] = [ 'date' => $date, 'views' => $dData['views'] ?? 0, 'human' => $dData['human'] ?? 0, 'uniques' => count($raw['uniques'][$date] ?? []) ]; } ksort($dailyChart); arsort($countries); arsort($pages); arsort($referrers); return [ 'totals' => $totals, 'countries' => $countries, 'pages' => $pages, 'referrers' => $referrers, 'daily_chart' => array_values($dailyChart) ]; } // Filtered by last $days days $cutoff = date('Y-m-d', strtotime("-{$days} days")); $filteredCountries = []; $filteredPages = []; $filteredTotals = ['views' => 0, 'human' => 0, 'bot' => 0, 'blocked' => 0, 'uniques' => 0]; $dailyChart = []; foreach ($raw['days'] ?? [] as $date => $dData) { if ($date >= $cutoff) { $v = $dData['views'] ?? 0; $h = $dData['human'] ?? 0; $b = $dData['bot'] ?? 0; $bl = $dData['blocked'] ?? 0; $u = count($raw['uniques'][$date] ?? []); $filteredTotals['views'] += $v; $filteredTotals['human'] += $h; $filteredTotals['bot'] += $b; $filteredTotals['blocked'] += $bl; $filteredTotals['uniques'] += $u; foreach ($dData['countries'] ?? [] as $c => $cnt) { $filteredCountries[$c] = ($filteredCountries[$c] ?? 0) + $cnt; } foreach ($dData['pages'] ?? [] as $p => $cnt) { $filteredPages[$p] = ($filteredPages[$p] ?? 0) + $cnt; } $dailyChart[$date] = [ 'date' => $date, 'views' => $v, 'human' => $h, 'uniques' => $u ]; } } // Fill missing dates in range for smooth chart for ($i = $days - 1; $i >= 0; $i--) { $dStr = date('Y-m-d', strtotime("-{$i} days")); if (!isset($dailyChart[$dStr])) { $dailyChart[$dStr] = ['date' => $dStr, 'views' => 0, 'human' => 0, 'uniques' => 0]; } } ksort($dailyChart); arsort($filteredCountries); arsort($filteredPages); $referrers = $raw['referrers'] ?? []; arsort($referrers); return [ 'totals' => $filteredTotals, 'countries' => $filteredCountries, 'pages' => $filteredPages, 'referrers' => $referrers, 'daily_chart' => array_values($dailyChart) ]; } }