Browsers (Firefox in particular) force-download application/rss+xml and application/atom+xml instead of rendering them, which is confusing for site visitors who click a feed link directly rather than pasting it into a reader. text/xml renders inline as an XML tree in every major browser. Feed readers (FreshRSS included) sniff the actual XML content to detect RSS/Atom, so this has no effect on subscription behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
144 lines
5.1 KiB
PHP
144 lines
5.1 KiB
PHP
<?php
|
|
/**
|
|
* Fonctions communes aux 3 flux RSS (actualites.php, agenda.php, facebook.php).
|
|
*
|
|
* Le site www.silly.be est derriere Cloudflare : son WAF bloque les
|
|
* requetes dont l'empreinte TLS/HTTP2 n'est pas "navigateur" (urllib
|
|
* Python passe, par ex., mais renvoie un corps vide). L'extension
|
|
* curl de PHP (libcurl) reproduit la meme empreinte que le binaire
|
|
* curl en ligne de commande, donc ca passe nativement.
|
|
*
|
|
* Autre contrainte observee sur l'API : b_size doit valoir exactement
|
|
* 20 (toute autre valeur renvoie un corps vide) ; la pagination se
|
|
* fait donc via b_start par pas de 20.
|
|
*/
|
|
|
|
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
const CACHE_DIR = __DIR__ . '/cache';
|
|
const CACHE_TTL_SECONDS = 900; // 15 minutes
|
|
|
|
/**
|
|
* Recupere une URL via curl (necessaire pour passer le WAF Cloudflare
|
|
* de silly.be) et renvoie le corps de la reponse en texte.
|
|
*/
|
|
function http_get(string $url, array $extraHeaders = []): string
|
|
{
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_USERAGENT => USER_AGENT,
|
|
CURLOPT_HTTPHEADER => array_merge(['Accept: application/json'], $extraHeaders),
|
|
CURLOPT_TIMEOUT => 20,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_MAXREDIRS => 3,
|
|
]);
|
|
$body = curl_exec($ch);
|
|
$errno = curl_errno($ch);
|
|
$error = curl_error($ch);
|
|
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($errno !== 0) {
|
|
throw new RuntimeException("curl error fetching $url: $error");
|
|
}
|
|
if ($status >= 400) {
|
|
throw new RuntimeException("HTTP $status fetching $url");
|
|
}
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Sert un contenu XML depuis un cache fichier, en le regenerant via
|
|
* $builder() si le cache est absent ou perime. Evite de solliciter
|
|
* silly.be / fetchrss.com a chaque poll de chaque abonne FreshRSS.
|
|
*/
|
|
function serve_cached_feed(string $cacheKey, callable $builder): void
|
|
{
|
|
// text/xml plutot que application/rss+xml : les navigateurs affichent
|
|
// ce type en ligne (arbre XML) au lieu de forcer un telechargement,
|
|
// ce qui est plus rassurant pour un visiteur qui clique le lien
|
|
// directement depuis le site. FreshRSS et les autres lecteurs RSS
|
|
// detectent le format en lisant le contenu, pas ce header.
|
|
header('Content-Type: text/xml; charset=utf-8');
|
|
|
|
if (!is_dir(CACHE_DIR)) {
|
|
mkdir(CACHE_DIR, 0775, true);
|
|
}
|
|
$cacheFile = CACHE_DIR . '/' . $cacheKey . '.xml';
|
|
|
|
if (is_file($cacheFile) && (time() - filemtime($cacheFile)) < CACHE_TTL_SECONDS) {
|
|
readfile($cacheFile);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$xml = $builder();
|
|
file_put_contents($cacheFile, $xml);
|
|
echo $xml;
|
|
} catch (Throwable $e) {
|
|
// En cas d'erreur (site source indisponible, etc.), on sert le
|
|
// cache perime plutot que de casser le flux chez les abonnes.
|
|
if (is_file($cacheFile)) {
|
|
readfile($cacheFile);
|
|
return;
|
|
}
|
|
http_response_code(502);
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
echo "Erreur de generation du flux : " . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
function strip_html(?string $text): string
|
|
{
|
|
if (!$text) {
|
|
return '';
|
|
}
|
|
$text = preg_replace('/<[^>]+>/', ' ', $text);
|
|
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
return trim(preg_replace('/\s+/', ' ', $text));
|
|
}
|
|
|
|
function rfc822(?string $isoDate): string
|
|
{
|
|
try {
|
|
$dt = $isoDate ? new DateTime($isoDate) : new DateTime('now', new DateTimeZone('UTC'));
|
|
} catch (Exception $e) {
|
|
$dt = new DateTime('now', new DateTimeZone('UTC'));
|
|
}
|
|
return $dt->format(DateTime::RSS);
|
|
}
|
|
|
|
function xml_escape(string $text): string
|
|
{
|
|
return htmlspecialchars($text, ENT_XML1 | ENT_QUOTES, 'UTF-8');
|
|
}
|
|
|
|
/**
|
|
* Construit un flux RSS 2.0 complet a partir d'une liste d'items deja
|
|
* formates (chacun un tableau title/link/guid/pubDate/description).
|
|
*/
|
|
function build_rss(string $title, string $link, string $description, array $items): string
|
|
{
|
|
$now = rfc822(null);
|
|
$xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
|
$xml .= '<rss version="2.0">' . "\n <channel>\n";
|
|
$xml .= ' <title>' . xml_escape($title) . "</title>\n";
|
|
$xml .= ' <link>' . xml_escape($link) . "</link>\n";
|
|
$xml .= ' <description>' . xml_escape($description) . "</description>\n";
|
|
$xml .= " <language>fr-be</language>\n";
|
|
$xml .= " <lastBuildDate>$now</lastBuildDate>\n";
|
|
|
|
foreach ($items as $item) {
|
|
$xml .= " <item>\n";
|
|
$xml .= ' <title>' . xml_escape($item['title']) . "</title>\n";
|
|
$xml .= ' <link>' . xml_escape($item['link']) . "</link>\n";
|
|
$xml .= ' <guid isPermaLink="false">' . xml_escape($item['guid']) . "</guid>\n";
|
|
$xml .= ' <pubDate>' . rfc822($item['pubDate'] ?? null) . "</pubDate>\n";
|
|
$xml .= ' <description>' . xml_escape($item['description']) . "</description>\n";
|
|
$xml .= " </item>\n";
|
|
}
|
|
|
|
$xml .= " </channel>\n</rss>\n";
|
|
return $xml;
|
|
}
|