40656631ba
- Admin stats : clic sur un réseau AS affiche les IPs avec mini sparkline 14 jours + articles/livres consultés - AccessLogParser : calcul ip_data (daily + top paths) inclus dans le cache stats - Suppression du tableau statique "Répartition par réseau" (fusionné dans accordéon pays) - PHP-CS-Fixer appliqué sur l'ensemble des fichiers modifiés Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
136 lines
5.6 KiB
PHP
136 lines
5.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
define('BASE_PATH', realpath(__DIR__ . '/../'));
|
|
|
|
require_once BASE_PATH . '/src/auth.php';
|
|
require_once BASE_PATH . '/src/SiteSettings.php';
|
|
require_once BASE_PATH . '/config/config.php';
|
|
require_once BASE_PATH . '/src/ArticleManager.php';
|
|
require_once BASE_PATH . '/src/TrendingParser.php';
|
|
|
|
// ── Périodes supportées ───────────────────────────────────────────────────────
|
|
|
|
const TRENDING_PERIODS = [
|
|
'10m' => ['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 '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
|
?>
|
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
<channel>
|
|
<title><?= $feedTitle ?></title>
|
|
<link><?= $baseXml ?></link>
|
|
<description><?= $descXml ?></description>
|
|
<language><?= $langXml ?></language>
|
|
<lastBuildDate><?= $buildDate ?></lastBuildDate>
|
|
<atom:link href="<?= $feedUrl ?>" rel="self" type="application/rss+xml"/>
|
|
<?php foreach ($items as ['article' => $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);
|
|
?>
|
|
<item>
|
|
<title><?= $title ?> (<?= $v ?> visiteur<?= $plural ?>)</title>
|
|
<link><?= $link ?></link>
|
|
<description><?= $desc ?></description>
|
|
<pubDate><?= $pubDate ?></pubDate>
|
|
<guid isPermaLink="true"><?= $link ?></guid>
|
|
</item>
|
|
<?php endforeach; ?>
|
|
</channel>
|
|
</rss>
|