You're welcome! Hope you patch it.Julius {l Wrote}:I just added a draft Privacy Policy that asks users for consent in regards to storage of IP, but I will consider patching it out as you described. Thanks for investigating that.
If you worry about that, then you shouldn't use Flarum in the first place. Downloading its frontpage makes hell a lot more (and lot worse, unoptimized) db queries than this little one. But, you can lower the limit to - let's say - 20 records. And I can add client-side cache management to it if you'd like.Julius {l Wrote}:I don't think running the rss feed as a live process with no caching is a good idea. RSS feeds get abused a lot and it could easily result in a denial of service attack kind of situation, even if maybe unintentional.
Yes, you miss the point that time differences are relative to the http query time.Julius {l Wrote}:I also don't really see the problem in having the rss feed a few minutes out of date by caching it in a .xml file. But maybe I am missing something in regards to how these relative time-stamps work in RSS feeds?
Now you're bluffing. If that were true, then Flarum couldn't read it's own configuration either.Julius {l Wrote}:Edit: and from the public directory it would not have access to the config file with the database credentials anyways I guess.
<?php
/* ------------------ set this to the Flarum config file -------------------- */
$config = @include("path/to/flarum/config.php");
$limit = 20; /* return that many records at most */
/* -------------------------- configuration ends ---------------------------- */
if(!is_array($config) || !is_array($config['database']))
die("Configuration not found.\n");
/***** connect to the database using the configuration in Flarum's config *****/
try {
$pdo = new \PDO(
$config['database']['driver'].":host=".$config['database']['host'].
";dbname=".$config['database']['database'].
(!empty($config['database']['charset']) ?
";charset=".$config['database']['charset'] : ""),
$config['database']['username'],
@$config['database']['password'],
[
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_EMULATE_PREPARES => 0
]);
} catch(\PDOException $e) {
die("Unable to connect to database: ".$e->getMessage()."\n");
}
if(!empty($config['database']['charset']))
$pdo->exec("set names '".$config['database']['charset']."';");
/********************** get some data from the database ***********************/
try {
$sql = "SELECT a.id, a.title, a.slug, a.last_posted_at, b.username, ".
" TIMESTAMPDIFF(SECOND, a.last_posted_at, NOW()) as t, ".
" DATE_FORMAT(a.last_posted_at, '%a, %d %b %Y %T') as d FROM ".
$config["database"]["prefix"]."discussions as a LEFT JOIN ".
$config["database"]["prefix"]."users as b ON a.last_posted_user_id = b.id ".
"WHERE b.id IS NOT NULL AND a.is_private != 1 ".
"ORDER BY a.last_posted_at DESC LIMIT ".$limit.";";
$res = $pdo->query($sql);
$rows = $res->fetchAll(\PDO::FETCH_ASSOC);
$pdo->close();
} catch(\PDOException $e) {
die("Unable to query the database: ".$e->getMessage()."\n");
}
/*************************** output what we've got ****************************/
/* tell the clients not to query this url more than once per minute */
header("Pragma:cache");
header("Cache-Control:cache,public,max-age=60");
header("Connection:close");
echo("<"."?xml version=\"1.0\" encoding=\"utf-8\"?".">\r\n");
?>
<rss version="2.0">
<channel>
<title>FreeGameDev</title>
<link><![CDATA[<?php echo($config["url"]); ?>]]</link>
<description>Free Game Development</description>
<?php foreach($rows as $row) { ?>
<item>
<title><![CDATA[<?php echo(htmlspecialchars($row["title"])); ?>]]></title>
<description><![CDATA[<?php
echo("by ".htmlspecialchars($row["username"])." ");
$t = $row["t"] + 0;
if($t < 60)
echo("just now");
else if($t < 60*60)
echo(($t/60)." minute".($t/60 > 1 ? "s" : "")." ago");
else if($t < 60*60*24)
echo(($t/60/60)." hour".($t/60/60 > 1 ? "s" : "")." ago");
else
echo(($t/60/60/24)." day".($t/60/60/24 > 1 ? "s" : "")." ago");
?>]]></description>
<guid><![CDATA[<?php echo(
/* create a link to the discussion. Slug might contain the id already */
$config["url"]."/d/".$row["id"]."-".$row["id"]."-".
urlencode($row["slug"]));
?>]]</guid>
<pubDate><![CDATA[<?php
echo(htmlspecialchars($row["d"]." ".date("T"))); ?>]]></pubDate>
</item>
<?php } ?>
</channel>
</rss>
Great!Julius {l Wrote}:I just now implemented a way to stop the IP logging that also survives forum upgrades, but your idea was a good hint on what to do. I just need to clean up the database of those IPs already logged and update the PP again.
Yeah it is, because Flarum does a lot more checks to access the cache and to see if cache needs update than what this little script does altogether. You see, it is so brainf*ck simple, that adding any caching and checking if that cache needs an update would actually increase it's overhead. (One possible way to implement a viable cache for it is to unconditionally update the rss cache when people post something, but that would require modifying Flarum's code here, which I'd like to avoid.)Julius {l Wrote}:I don't think loading the Flarum main page is quite comparable as it is heavily cached and people don't constantly spam the refresh button in their browser like some RSS clients seem to do by default.
It is RFC. All feed readers must accept it. While true, there can be badly written feed readers sure, assuming that a feed reader isn't RFC compliant is like assuming the users won't spam the refresh button (I would say the chances are much higher for the latter).Julius {l Wrote}:So this RSS client side access limts that you added now... do you know how widely such client recommendations are supported by RSS feed readers?
Now why would it leak the server internal path? First, you can use a relative path. Second, it never outputs that path to the user, so no leak possible. I've even added a surpress operator to the include so that if you specify an incorrect path, php error message wouldn't leak that incorrect path either. I've also payed attention not to print any sensitive information in the error messages. I have about 20 years of experience with high-security and high-available, load-balanced web-clusters you know :-D But it's okay that you don't trust my code, you don't know me, after all. Do your tests, check whatever you want to check. Let me know if you find anything.Julius {l Wrote}:I am not afraid of your code, it seems perfectly fine for what it attempts to do. I am just not sure if this specific implementation is a good idea for the reasons outlined above. As for the "public" directory... yeah I am not 100% sure what and what not that restricts I guess I would need to try it. But at the very least your script would leak the server internal path of the config file which is not ideal either.
$pdo->db = null;
That's strange, the PDO driver should have a close method. Anyway, I made it optional.Julius {l Wrote}:Hmm, so that doesn't work very well tbh.
I got it to a semi-functional state by fixing line 40 as
- {l Code}: {l Select All Code}
$pdo->db = null;
Uppps, I forgot to properly close one of them, mea culpa. Fixed.Julius {l Wrote}:and removing all the CDATA parts (not exactly sure why if fails completely with those)
What do you mean by "not very useful"? I deliberately not cited the post's content to spare resources, only the topic's title and the post time and poster user. I can add more fields, no probs, but that would increase the script's load.Julius {l Wrote}:and at least now I get something that some feed-readers interpret as a RSS feed (but not a very useful one).
Okay, I've fixed all of these.Julius {l Wrote}:However it is still far from a valid rss feed: https://validator.w3.org/feed/check.cgi ... %2Frss.php
<?php
/* ------------------ set this to the Flarum config file -------------------- */
$config = @include("path/to/flarum/config.php");
$limit = 20; /* return that many records at most */
/* -------------------------- configuration ends ---------------------------- */
if(!is_array($config) || !is_array($config['database']))
die("Configuration not found.\n");
/***** connect to the database using the configuration in Flarum's config *****/
try {
$pdo = new \PDO(
$config['database']['driver'].":host=".$config['database']['host'].
";dbname=".$config['database']['database'].
(!empty($config['database']['charset']) ?
";charset=".$config['database']['charset'] : ""),
$config['database']['username'],
@$config['database']['password'],
[
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_EMULATE_PREPARES => 0
]);
} catch(\PDOException $e) {
die("Unable to connect to database: ".$e->getMessage()."\n");
}
if(!empty($config['database']['charset']))
$pdo->exec("set names '".$config['database']['charset']."';");
/********************** get some data from the database ***********************/
try {
$sql = "SELECT a.id, a.title, a.slug, a.last_posted_at, b.username, ".
" TIMESTAMPDIFF(SECOND, a.last_posted_at, NOW()) as t, ".
" DATE_FORMAT(a.last_posted_at, '%a, %d %b %Y %T') as d FROM ".
$config["database"]["prefix"]."discussions as a LEFT JOIN ".
$config["database"]["prefix"]."users as b ON a.last_posted_user_id = b.id ".
"WHERE b.id IS NOT NULL AND a.is_private != 1 ".
"ORDER BY a.last_posted_at DESC LIMIT ".$limit.";";
$res = $pdo->query($sql);
$rows = $res->fetchAll(\PDO::FETCH_ASSOC);
if(method_exists($pdo, "close"))
$pdo->close();
else
$pdo->db = null;
} catch(\PDOException $e) {
die("Unable to query the database: ".$e->getMessage()."\n");
}
/*************************** output what we've got ****************************/
/* tell the clients not to query this url more than once per minute */
header("Pragma:cache");
header("Cache-Control:cache,public,max-age=60");
header("Connection:close");
header("Content-Type: application/rss+xml");
echo("<"."?xml version=\"1.0\" encoding=\"utf-8\"?".">\r\n");
?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>FreeGameDev</title>
<link><![CDATA[<?php echo($config["url"]); ?>]]></link>
<description>Free Game Development</description>
<atom:link href="<?php echo($config["url"]); ?>/rss.php" rel="self" type="application/rss+xml" />
<?php foreach($rows as $row) { ?>
<item>
<title><![CDATA[<?php echo(htmlspecialchars($row["title"])); ?>]]></title>
<description><![CDATA[<?php
echo("by ".htmlspecialchars($row["username"])." ");
$t = $row["t"] + 0;
if($t < 60)
echo("just now");
else if($t < 60*60)
echo(floor($t/60)." minute".($t/60 > 1 ? "s" : "")." ago");
else if($t < 60*60*24)
echo(floor($t/60/60)." hour".($t/60/60 > 1 ? "s" : "")." ago");
else
echo(floor($t/60/60/24)." day".($t/60/60/24 > 1 ? "s" : "")." ago");
?>]]></description>
<guid><![CDATA[<?php echo(
$config["url"]."/d/".$row["id"]."-".$row["id"]."-".
urlencode($row["slug"]));
?>]]></guid>
<pubDate><![CDATA[<?php
echo(htmlspecialchars($row["d"]." ".date("O"))); ?>]]></pubDate>
</item>
<?php } ?>
</channel>
</rss>
Good to hear and you're welcome!Julius {l Wrote}:Yes that looks significantly better now that the time-stamp actually works. Thanks!
I've added the "new topic" / "reply", that was easy. On the other hand, I would not recommend post contents. First, they would require much more resource, both on the database side on the network traffic. Second, if Flarum allows formatting, then posts would be shown with non-interpreted html tags (not good), and it would require further resources to parse and remove those tags. But can be done, let me know if despite this you want post content in the feed.Julius {l Wrote}:I will test it a bit more and see if it can be further improved. Maybe including the post content or at least clearly differentiating between new topics and replies would make sense though.
Haha, told you. That's why I calculate the elapsed time from strictly the database time.Julius {l Wrote}:There is a problem with timezones it seems. I post that I just now made shows up as some hours ago in the feed due to difference in timezone to the server in Germany?
- echo(htmlspecialchars($row["d"]." +0000")); ?>]]></pubDate>
+ echo(htmlspecialchars($row["d"]." ".date("O"))); ?>]]></pubDate>
<?php
/* ------------------ set this to the Flarum config file -------------------- */
$config = @include("path/to/flarum/config.php");
$limit = 20; /* return that many records at most */
/* -------------------------- configuration ends ---------------------------- */
if(!is_array($config) || !is_array($config['database']))
die("Configuration not found.\n");
/***** connect to the database using the configuration in Flarum's config *****/
try {
$pdo = new \PDO(
$config['database']['driver'].":host=".$config['database']['host'].
";dbname=".$config['database']['database'].
(!empty($config['database']['charset']) ?
";charset=".$config['database']['charset'] : ""),
$config['database']['username'],
@$config['database']['password'],
[
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_EMULATE_PREPARES => 0
]);
} catch(\PDOException $e) {
die("Unable to connect to database: ".$e->getMessage()."\n");
}
if(!empty($config['database']['charset']))
$pdo->exec("set names '".$config['database']['charset']."';");
/********************** get some data from the database ***********************/
try {
$sql = "SELECT a.id, a.title, a.slug, a.last_posted_at, a.comment_count, b.username, ".
" TIMESTAMPDIFF(SECOND, a.last_posted_at, NOW()) as t, ".
" DATE_FORMAT(a.last_posted_at, '%a, %d %b %Y %T') as d FROM ".
$config["database"]["prefix"]."discussions as a LEFT JOIN ".
$config["database"]["prefix"]."users as b ON a.last_posted_user_id = b.id ".
"WHERE b.id IS NOT NULL AND a.is_private != 1 ".
"ORDER BY a.last_posted_at DESC LIMIT ".$limit.";";
$res = $pdo->query($sql);
$rows = $res->fetchAll(\PDO::FETCH_ASSOC);
if(method_exists($pdo, "close"))
$pdo->close();
else
$pdo->db = null;
} catch(\PDOException $e) {
die("Unable to query the database: ".$e->getMessage()."\n");
}
/*************************** output what we've got ****************************/
/* tell the clients not to query this url more than once per minute */
header("Pragma:cache");
header("Cache-Control:cache,public,max-age=60");
header("Connection:close");
header("Content-Type: application/rss+xml");
echo("<"."?xml version=\"1.0\" encoding=\"utf-8\"?".">\r\n");
?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>FreeGameDev</title>
<link><![CDATA[<?php echo($config["url"]); ?>]]></link>
<description>Free Game Development</description>
<atom:link href="<?php echo($config["url"]); ?>/rss.php" rel="self" type="application/rss+xml" />
<?php foreach($rows as $row) { ?>
<item>
<title><![CDATA[<?php echo(htmlspecialchars($row["title"])); ?>]]></title>
<description><![CDATA[<?php
echo(($row["comment_count"] < 2 ? "new topic" : "reply").
" by ".htmlspecialchars($row["username"])." ");
$t = $row["t"] + 0; $m = floor($t/60); $h = floor($t/60/60); $d = floor($t/60/60/24);
if($t < 60)
echo("just now");
else if($t < 60*60)
echo($m." minute".($m > 1 ? "s" : "")." ago");
else if($t < 60*60*24)
echo($h." hour".($h > 1 ? "s" : "")." ago");
else
echo($d." day".($d > 1 ? "s" : "")." ago");
?>]]></description>
<guid><![CDATA[<?php echo(
$config["url"]."/d/".$row["id"]."-".$row["id"]."-".
urlencode($row["slug"]));
?>]]></guid>
<pubDate><![CDATA[<?php
echo(htmlspecialchars($row["d"]." +0000")); ?>]]></pubDate>
</item>
<?php } ?>
</channel>
</rss>
That's nothing, it is in my best interest to get this working :-)Julius {l Wrote}:Thanks for looking into it another time.
Then your RSS reader is buggy, and can't convert from server's timezone to yours, but it can convert from UTC to yours.Julius {l Wrote}:The time in the feed reader seems to be correct now, but the one that is mentioned in the message body itself is still incorrect.
- " TIMESTAMPDIFF(SECOND, a.last_posted_at, NOW()) as t, ".
+ " TIMESTAMPDIFF(SECOND, a.last_posted_at, CURRENT_TIMESTAMP()) as t, ".
It will be fine when all date times will be in UTC.Julius {l Wrote}:Hmm, ok. I am just using Thunderbird to test the feed, so maybe that isn't the best feed reader.
Yes, that's a timezone issue, that using CURRENT_TIMESTAMP will solve. About not updating I don't know, could be a caching issue? Let's solve timezone first, and then I'll investigate that.Julius {l Wrote}:But what I actually meant is the time mentioned in the message body itself, for example when it says: "reply by user 2 hours ago". That time has both an timezone issue and also never seems to update (maybe due to client side caching?).
$ curl -I https://freegamedev.net/rss.php
HTTP/2 200
server: nginx
date: Wed, 09 Jun 2021 14:54:30 GMT
content-type: application/rss+xml
pragma: cache
cache-control: cache,public,max-age=60
cache-control: max-age=3600
expires: Wed, 09 Jun 2021 15:54:30 GMT
$ curl -I https://freegamedev.net/
HTTP/2 200
server: nginx
date: Wed, 09 Jun 2021 15:05:10 GMT
content-type: text/html; charset=utf-8
vary: Accept-Encoding
x-content-type-options: nosniff
referrer-policy: same-origin
x-powered-by: Flarum
x-csrf-token: fBfViSOoOEoSIPPzaZWmQOVmntjLsjhbqvPOz3BN
set-cookie: flarum_session=LKyeud1qJXC3wkV9n8s4IiWiU4xAydfcAJMwR3Bq; Path=/; Expires=Wed, 09 Jun 2021 17:05:10 GMT; Max-Age=7200; Secure; HttpOnly; SameSite=Lax
cache-control: max-age=0
expires: Wed, 09 Jun 2021 15:05:10 GMT
location ~ rss\.php {
expire 1m;
}
That's already taken care of. If you take a closer look at the sql, it has "a.is_private != 1".Julius {l Wrote}:P.S.: Any good suggestion how to not query tags that are hidden? Right now we don't use any, but in theory your script would also query those and thus expose such posts to non-authorized users.
- " TIMESTAMPDIFF(SECOND, a.last_posted_at, CURRENT_TIMESTAMP()) as t, ".
- " DATE_FORMAT(a.last_posted_at, '%a, %d %b %Y %T') as d FROM ".
+ " TIMESTAMPDIFF(SECOND, a.last_posted_at, NOW()) as t, ".
+ " DATE_FORMAT(CONVERT_TZ(a.last_posted_at, 'SYSTEM', 'UTC'), '%a, %d %b %Y %T') as d FROM ".
Then the original code was correct.Julius {l Wrote}:Database is probably CEST, but didn't check.
This is more than strange, CONVERT_TZ not working, are you kidding? Then the only solution is to go back to the original code (with "date("O")" and "NOW"), and for you to use a feed reader that can actually understand timezones in pubDate.Julius {l Wrote}:I made the two changes as you outlined, but now the actual date in the feed reader is broken again as you can likely see.
Edit: and it doesn't validate: https://validator.w3.org/feed/check.cgi ... %2Frss.php
What do you mean by "tag"? There's no such thing in Flarum, there are discussions and posts. Both can be made hidden, is that what you mean by private tag? Private discussions are taken care of, I'm 100% sure of that. Anyway I've added more checks for the hidden fields too.Julius {l Wrote}:I just tested the private tags and they do show up in the rss feed. I guess another bugfix is needed for that. Thanks for looking into that as well
<?php
/* ------------------ set this to the Flarum config file -------------------- */
$config = @include("path/to/flarum/config.php");
$limit = 20; /* return that many records at most */
/* -------------------------- configuration ends ---------------------------- */
if(!is_array($config) || !is_array($config['database']))
die("Configuration not found.\n");
/***** connect to the database using the configuration in Flarum's config *****/
try {
$pdo = new \PDO(
$config['database']['driver'].":host=".$config['database']['host'].
";dbname=".$config['database']['database'].
(!empty($config['database']['charset']) ?
";charset=".$config['database']['charset'] : ""),
$config['database']['username'],
@$config['database']['password'],
[
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_EMULATE_PREPARES => 0
]);
} catch(\PDOException $e) {
die("Unable to connect to database: ".$e->getMessage()."\n");
}
if(!empty($config['database']['charset']))
$pdo->exec("set names '".$config['database']['charset']."';");
/********************** get some data from the database ***********************/
try {
$sql = "SELECT a.id, a.title, a.slug, a.last_posted_at, a.comment_count, b.username, ".
" TIMESTAMPDIFF(SECOND, a.last_posted_at, NOW()) as t, ".
" DATE_FORMAT(a.last_posted_at, '%a, %d %b %Y %T') as d FROM ".
$config["database"]["prefix"]."discussions as a LEFT JOIN ".
$config["database"]["prefix"]."users as b ON a.last_posted_user_id = b.id ".
"WHERE b.id IS NOT NULL AND b.hidden_at IS NULL AND a.hidden_at IS NULL".
" AND a.is_private != 1 AND b.is_private != 1 ".
"ORDER BY a.last_posted_at DESC LIMIT ".$limit.";";
$res = $pdo->query($sql);
$rows = $res->fetchAll(\PDO::FETCH_ASSOC);
if(method_exists($pdo, "close"))
$pdo->close();
else
$pdo->db = null;
} catch(\PDOException $e) {
die("Unable to query the database: ".$e->getMessage()."\n");
}
/*************************** output what we've got ****************************/
/* tell the clients not to query this url more than once per minute */
header("Pragma:cache");
header("Cache-Control:cache,public,max-age=60");
header("Connection:close");
header("Content-Type: application/rss+xml");
echo("<"."?xml version=\"1.0\" encoding=\"utf-8\"?".">\r\n");
?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>FreeGameDev</title>
<link><![CDATA[<?php echo($config["url"]); ?>]]></link>
<description>Free Game Development</description>
<atom:link href="<?php echo($config["url"]); ?>/rss.php" rel="self" type="application/rss+xml" />
<?php foreach($rows as $row) { ?>
<item>
<title><![CDATA[<?php echo(htmlspecialchars($row["title"])); ?>]]></title>
<description><![CDATA[<?php
echo(($row["comment_count"] < 2 ? "new topic" : "reply").
" by ".htmlspecialchars($row["username"])." ");
$t = $row["t"] + 0; $m = floor($t/60); $h = floor($t/60/60); $d = floor($t/60/60/24);
if($t < 60)
echo("just now");
else if($t < 60*60)
echo($m." minute".($m > 1 ? "s" : "")." ago");
else if($t < 60*60*24)
echo($h." hour".($h > 1 ? "s" : "")." ago");
else
echo($d." day".($d > 1 ? "s" : "")." ago");
?>]]></description>
<guid><![CDATA[<?php echo(
$config["url"]."/d/".$row["id"]."-".$row["id"]."-".
urlencode($row["slug"]));
?>]]></guid>
<pubDate><![CDATA[<?php
echo(htmlspecialchars($row["d"]." ".date("O")); ?>]]></pubDate>
</item>
<?php } ?>
</channel>
</rss>
Whatever sails your boat that makes it possible to list new posts without enabling JS.Julius {l Wrote}:Looks like the originally proposed Flarum extenson got an update after all: https://discuss.flarum.org/d/27687-synd ... atom-feeds
I'll try it as an alternative later today.
Aaaaargggh, copy'n'paste bug... One less closing parenthesis.Julius {l Wrote}:Your new version fails completely. Don't have time right now to try and find the error though. I left it up in the broken state for now.
- echo(htmlspecialchars($row["d"]." ".date("O")); ?>]]></pubDate>
+ echo(htmlspecialchars($row["d"]." ".date("O"))); ?>]]></pubDate>
Users browsing this forum: No registered users and 1 guest