true, CURLOPT_USERAGENT => USER_AGENT, CURLOPT_HTTPHEADER => array_merge(['Accept: application/json'], $extraHeaders), CURLOPT_TIMEOUT => 20, CURLOPT_FOLLOWLOCATION => true, ]); $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 { header('Content-Type: application/rss+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 = '' . "\n"; $xml .= '' . "\n \n"; $xml .= ' ' . xml_escape($title) . "\n"; $xml .= ' ' . xml_escape($link) . "\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; }