<?php define("VERSION", "0.0.1");
/* --- FEEDS - EDIT AS NEEDED --- */
$feeds["OTW News"] ["url"] = "https://www.transformativeworks.org/category/announcement/feed/";
$feeds["Dreamwidth News"]["url"] = "https://dw-news.dreamwidth.org/data/rss";
/* --- CONFIG - EDIT AS NEEDED --- */
/// Directory to store RSS cache.
///
/// Multiple instances can share one dir.
$config["cache_dir"] = "/tmp/rss_dot_php";
/// Custom CSS
$config["custom_css"] = <<<'EOC'
/* custom CSS goes here! */
EOC;
/// Document Language
$config["lang"] = "en";
/// Date Format
///
/// Displayed under every article, see
/// <https://www.php.net/manual/en/datetime.format.php>
/// for documentation.
$config["date_fmt"] = "l, M jS, Y, H:i T";
/// Timezone
///
/// A value of type DateTimeZone, see
/// <https://www.php.net/manual/en/class.datetimezone.php>
/// for documentation.
$config["timezone"] = new DateTimeZone('UTC');
/* --- CODE - DO NOT TOUCH --- */
function load_rss(string $uri, string $linkrel = "alternate"): array {
global $config;
$xml = file_get_contents($uri);
// if the file doesn't contain an encoding, attempt to read it from http headers and re-encode
if (!preg_match("/^[^>]+encoding/", $xml) && str_starts_with($uri, "http")) {
foreach ($http_response_header as $header) {
if (!str_starts_with(strtolower($header), "content-type")) continue;
if (preg_match("/(?<=charset=)[a-z0-9_-]+/i", $header, $matches)) {
$xml = iconv($matches[0], "UTF-8", $xml);
$doc = new DOMDocument(encoding: "UTF-8");
}
break;
}
}
$doc ??= new DOMDocument();
$doc->loadXML($xml);
if ($doc->documentElement->nodeName == "rss") {
// TODO: better rss / atom sniffing
foreach ($doc->getElementsByTagName("item") as $node) {
$data["title"] = $node->getElementsByTagName("title")
?->item(0)?->textContent;
$data["link"] ??= $node->getElementsByTagName("link")
?->item(0)?->textContent;
$data["date"] = new DateTime($node->getElementsByTagName("pubDate")
->item(0)->textContent);
$data["date"]->setTimezone($config["timezone"]);
$parsed[] = $data;
}
} else {
// assume atom
foreach ($doc->getElementsByTagName("entry") as $node) {
$data["title"] = $node->getElementsByTagName("title")
?->item(0)?->textContent;
$data["links"] = [];
foreach ($node->getElementsByTagName("link")->getIterator() as $link) {
$date["links"][] = ["rel" => $link->getAttribute("rel"),
"href" => $link->getAttribute("href")];
if ($link->getAttribute("rel") === $linkrel) {
$data["link"] ??= $link->getAttribute("href");
}
}
$data["link"] ??= @$data["links"][0];
$data["date"] = $node->getElementsByTagName("published")
?->item(0)?->textContent;
$data["date"] ??= $node->getElementsByTagName("updated")
?->item(0)?->textContent;
$data["date"] = new DateTime($data["date"]);
$data["date"]->setTimezone($config["timezone"]);
$parsed[] = $data;
}
}
return $parsed??[];
}
function load_cached(int $ttl, string $uri, string $linkrel = "alternate"): array {
global $config;
$path = $config["cache_dir"]."/".md5($uri);
// echo $path."\n";
if ((@filemtime($path) ?? 0) + $ttl < time()) {
// echo "cache miss, loading over network\n";
$data = load_rss($uri, $linkrel);
file_put_contents($path, serialize($data));
return $data;
} else {
// echo "cache hit, loading from file\n";
return unserialize(file_get_contents($path));
}
}
@mkdir($config["cache_dir"], recursive: true);
foreach ($_GET["disabled"]??[] as $idx => $feed) {
if (!array_key_exists($feed, $feeds)) {
unset($_GET["disabled"][$idx]);
continue;
}
$off_feeds[$feed] = @$feeds[$feed];
unset($feeds[$feed]);
}
$combined = [];
// Real Feed Processing Happens Here
foreach ($feeds as $name => $data) {
if (!isset($data["url"])) {
error_log("Feed \"$name\" missing url. Ignoring.");
continue;
}
if (!isset($data["ttl"])) $data["ttl"] = 3600;
if (!isset($data["linkrel"])) $data["linkrel"] = "alternate";
foreach(load_cached($data["ttl"], $data["url"], $data["linkrel"]) as $entry) {
$entry["source"] = $name;
$combined[] = $entry;
}
}
// reverse-chronological by default
usort($combined, fn($a, $b) => $b["date"]->getTimestamp() <=> $a["date"]->getTimestamp());
if (isset($_GET["reverse"]))
$combined = array_reverse($combined);
$base = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
?>
<!doctype html>
<html lang="<?= $config['lang'] ?>">
<head>
<meta charset="utf-8">
<style><?= $config['custom_css'] ?></style>
</head>
<body>
<nav>
<div>
<b>Toggle Feeds</b>:
<?php foreach ($feeds??[] as $name => $data):
$query = $_GET;
$query["disabled"][] = $name;
$uri = $base."?".http_build_query($query);
?>
<span class="source" data-source="<?= htmlentities($name) ?>">
<a href="<?= htmlentities($uri) ?>"><?= htmlentities($name) ?></a>
</span>
<?php endforeach; ?>
<?php foreach ($off_feeds??[] as $name => $data):
$query = $_GET;
$query["disabled"] = array_filter($query["disabled"], fn($x) => $x !== $name);
$uri = $base."?".http_build_query($query);
?>
<span class="source disabled" data-source="<?= htmlentities($name) ?>">
<a href="<?= htmlentities($uri) ?>"><?= htmlentities($name) ?></a>
</span>
<?php endforeach; ?>
</div>
</nav>
<main>
<?php if (!count($combined) && isset($_GET['disabled'])): ?>
<h1>Looks like you filtered out everything...</h1>
<p>Try unfiltering some feeds!</p>
<?php endif;
foreach ($combined as $entry): ?>
<article>
<h1><a href="<?= htmlentities($entry['link']) ?>"><?= htmlentities($entry['title'] ?? "[[[No Title]]]") ?></a></h1>
<span class="source" data-source="<?= htmlentities($entry['source']) ?>"><?= htmlentities($entry['source']) ?></span>
•
<time datetime="<?= htmlentities($entry['date']->format(DateTime::ISO8601)) ?>">
<?= htmlentities($entry['date']->format($config['date_fmt'])) ?>
</time>
</article>
<?php endforeach; ?>
</main>
<!-- generated by rss_dot_php <?= VERSION ?>
https://git.aleteoryx.me/cgit/rss_dot_php -->
</body>
</html>