['seconds' => 600, 'label' => '10 dernières minutes'], '20m' => ['seconds' => 1200, 'label' => '20 dernières minutes'], '30m' => ['seconds' => 1800, 'label' => '30 dernières minutes'], '1h' => ['seconds' => 3600, 'label' => 'dernière heure'], '8h' => ['seconds' => 28800, 'label' => '8 dernières heures'], '1d' => ['seconds' => 86400, 'label' => '24 dernières heures'], '7d' => ['seconds' => 604800, 'label' => '7 derniers jours'], '14d' => ['seconds' => 1209600, 'label' => '14 derniers jours'], '30d' => ['seconds' => 2592000, 'label' => '30 derniers jours'], '1y' => ['seconds' => 31536000, 'label' => 'dernière année'], ]; $period = $_GET['period'] ?? '1d'; if (!array_key_exists($period, TRENDING_PERIODS)) { http_response_code(400); header('Content-Type: text/plain; charset=UTF-8'); echo 'Période invalide. Valeurs acceptées : ' . implode(', ', array_keys(TRENDING_PERIODS)); exit; } $seconds = TRENDING_PERIODS[$period]['seconds']; $label = TRENDING_PERIODS[$period]['label']; $cutoff = time() - $seconds; $cacheTtl = max(60, min(28800, (int) ($seconds / 5))); // ── Cache ───────────────────────────────────────────────────────────────────── @mkdir(DATA_PATH . '/_cache', 0755, true); $cacheFile = DATA_PATH . '/_cache/trending_' . $period . '.json'; $topPaths = null; if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTtl) { $topPaths = json_decode((string) file_get_contents($cacheFile), true) ?: null; } if ($topPaths === null) { $parser = new TrendingParser('/var/log/apache2', apacheAccessLog()); $topPaths = $parser->top($cutoff, 50); @file_put_contents($cacheFile, json_encode($topPaths)); } // ── Index slug → article (publiés, non privés) ──────────────────────────────── $articleManager = new ArticleManager(DATA_PATH); $now = time(); $privateCats = $articleManager->getPrivateCategories(); $slugIndex = []; foreach ($articleManager->getAll(publishedOnly: true) as $a) { if (strtotime((string) ($a['published_at'] ?? '')) > $now) { continue; } $cat = trim($a['category'] ?? ''); if ($cat !== '' && in_array($cat, $privateCats, true)) { continue; } $slugIndex[$a['slug']] = $a; } // ── Construction des items ──────────────────────────────────────────────────── $base = rtrim(APP_URL, '/'); $items = []; foreach ($topPaths as $path => $visitors) { if (!preg_match('#^/post/([^/]+)$#', $path, $m)) { continue; } $slug = rawurldecode($m[1]); $article = $slugIndex[$slug] ?? null; if ($article === null) { continue; } $items[] = ['article' => $article, 'visitors' => (int) $visitors]; if (count($items) >= 50) { break; } } // ── Réponse RSS ─────────────────────────────────────────────────────────────── header('Content-Type: application/rss+xml; charset=UTF-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: public, max-age=' . $cacheTtl); $feedTitle = htmlspecialchars(siteTitle() . ' — Tendances (' . $label . ')', ENT_XML1); $feedUrl = htmlspecialchars($base . '/trending?period=' . rawurlencode($period), ENT_XML1); $baseXml = htmlspecialchars($base, ENT_XML1); $buildDate = htmlspecialchars(date(DATE_RSS)); $descXml = htmlspecialchars('Top 50 articles par visiteurs uniques — ' . $label, ENT_XML1); $langXml = htmlspecialchars(siteLang(), ENT_XML1); echo '' . "\n"; ?> <?= $feedTitle ?> $a, 'visitors' => $v]): $link = htmlspecialchars($base . '/post/' . rawurlencode($a['slug']), ENT_XML1); $pubDate = htmlspecialchars(date(DATE_RSS, (int) strtotime((string) ($a['published_at'] ?? $a['created_at'] ?? '')))); $title = htmlspecialchars(($a['title'] ?? ''), ENT_XML1); $plural = $v > 1 ? 's' : ''; $desc = htmlspecialchars($title . ' — ' . $v . ' visiteur' . $plural . ' unique' . $plural . ' (' . $label . ')', ENT_XML1); ?> <?= $title ?> (<?= $v ?> visiteur<?= $plural ?>)