true, CURLOPT_USERAGENT => USER_AGENT, CURLOPT_HTTPHEADER => $headers, 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, int $ttl = CACHE_TTL_SECONDS): 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)) < $ttl) { 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 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). * $selfUrl est l'URL du flux lui-meme (atom:link rel="self", recommande * par le validateur W3C). */ function build_rss(string $title, string $link, string $description, array $items, string $selfUrl): string { $now = rfc822(null); $xml = '' . "\n"; $xml .= '' . "\n \n"; $xml .= ' ' . xml_escape($title) . "\n"; $xml .= ' ' . xml_escape($link) . "\n"; $xml .= ' ' . "\n"; $xml .= ' ' . xml_escape($description) . "\n"; $xml .= " fr-be\n"; $xml .= " $now\n"; foreach ($items as $item) { $xml .= " \n"; $xml .= ' ' . xml_escape($item['title']) . "\n"; $xml .= ' ' . xml_escape($item['link']) . "\n"; $xml .= ' ' . xml_escape($item['guid']) . "\n"; $xml .= ' ' . rfc822($item['pubDate'] ?? null) . "\n"; $xml .= ' ' . xml_escape($item['description']) . "\n"; $xml .= " \n"; } $xml .= " \n\n"; return $xml; }