"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Omar Polo <op@omarpolo.com>
Subject:
gotwebd: rss feeds for repo tags
To:
gameoftrees@openbsd.org
Date:
Mon, 19 Dec 2022 12:45:28 +0100

Download raw body.

Thread
Hello,

I'm subscribed to several rss feeds (for e.g. release feeds on github)
and I think it would be nice if gotwebd could provide an RSS feed for
the tags.

Basically, it's just an alternate way of rendering the tags page.

To do so I had to define an helper function
`gotweb_render_absolute_url' since the RSS spec[0] as far as I can
tell wants absolute URLs in some places.  In doing so I discovered
that my idea of using SCRIPT_NAME for the "prefix" doesn't really
work, with the configuration per the manpage httpd sends an empty
string as SCRIPT_NAME.  DOCUMENT_URI is much better, so switch to it
too.  I also needed to know whether we're over https or plain http.

(will land this change as separate commit if ok)

I've tested it locally where I'm running gotwebd under a path prefix
and on my server[1], seems to work just fine.  I've tested the feeds
with rssgoemail, liferea, and a validator[2], and all are happy.

What do you think?

[0]: https://www.rssboard.org/rss-specification
[1]: https://git.omarpolo.com/
[2]: https://www.rssboard.org/rss-validator/check.cgi

diff /home/op/w/got
commit - b2b1792329f5adf1e755614e710d49388845293e
path + /home/op/w/got
blob - b4f85fc2121de39b9a1c7ff2f9fcab75f5b7da42
file + gotwebd/fcgi.c
--- gotwebd/fcgi.c
+++ gotwebd/fcgi.c
@@ -261,12 +261,12 @@ fcgi_parse_params(uint8_t *buf, uint16_t n, struct req
 				*sd = '\0';
 		}
 
-		if (c->script_name[0] == '\0' &&
-		    val_len < MAX_SCRIPT_NAME &&
-		    name_len == 11 &&
-		    strncmp(buf, "SCRIPT_NAME", 11) == 0) {
-			memcpy(c->script_name, val, val_len);
-			c->script_name[val_len] = '\0';
+		if (c->document_uri[0] == '\0' &&
+		    val_len < MAX_DOCUMENT_URI &&
+		    name_len == 12 &&
+		    strncmp(buf, "DOCUMENT_URI", 12) == 0) {
+			memcpy(c->document_uri, val, val_len);
+			c->document_uri[val_len] = '\0';
 		}
 
 		if (c->server_name[0] == '\0' &&
@@ -277,6 +277,10 @@ fcgi_parse_params(uint8_t *buf, uint16_t n, struct req
 			c->server_name[val_len] = '\0';
 		}
 
+		if (name_len == 5 &&
+		    strncmp(buf, "HTTPS", 5) == 0)
+			c->https = 1;
+
 		buf += name_len + val_len;
 		n -= name_len - val_len;
 	}
blob - 50fee3056b1e5f7a9bc78f255201d8b1539a2995
file + gotwebd/got_operations.c
--- gotwebd/got_operations.c
+++ gotwebd/got_operations.c
@@ -572,7 +572,7 @@ got_get_repo_tags(struct request *c, int limit)
 	    repo_dir->name) == -1)
 		return got_error_from_errno("asprintf");
 
-	if (qs->commit == NULL && qs->action == TAGS) {
+	if (qs->commit == NULL && (qs->action == TAGS || qs->action == RSS)) {
 		error = got_ref_open(&ref, repo, qs->headref, 0);
 		if (error)
 			goto err;
blob - 28042467bba28194c9543f1b05ca9cfc5cfd644e
file + gotwebd/gotweb.c
--- gotwebd/gotweb.c
+++ gotwebd/gotweb.c
@@ -50,6 +50,7 @@
 
 #include "proc.h"
 #include "gotwebd.h"
+#include "tmpl.h"
 
 static const struct querystring_keys querystring_keys[] = {
 	{ "action",		ACTION },
@@ -74,6 +75,7 @@ static const struct action_keys action_keys[] = {
 	{ "tag",	TAG },
 	{ "tags",	TAGS },
 	{ "tree",	TREE },
+	{ "rss",	RSS },
 };
 
 static const struct got_error *gotweb_init_querystring(struct querystring **);
@@ -177,6 +179,24 @@ render:
 		goto done;
 	}
 
+	if (qs->action == RSS) {
+		error = gotweb_render_content_type(c,
+		    "application/rss+xml;charset=utf-8");
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+
+		error = got_get_repo_tags(c, D_MAXSLCOMMDISP);
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		if (gotweb_render_rss(c->tp) == -1)
+			goto err;
+		goto done;
+	}
+
 render:
 	error = gotweb_render_content_type(c, "text/html");
 	if (error) {
@@ -1624,6 +1644,8 @@ gotweb_action_name(int action)
 		return "tags";
 	case TREE:
 		return "tree";
+	case RSS:
+		return "rss";
 	default:
 		return NULL;
 	}
@@ -1722,6 +1744,21 @@ gotweb_link(struct request *c, struct gotweb_url *url,
 }
 
 int
+gotweb_render_absolute_url(struct request *c, struct gotweb_url *url)
+{
+	struct template	*tp = c->tp;
+	const char	*proto = c->https ? "http" : "http";
+
+	if (fcgi_puts(tp, proto) == -1 ||
+	    fcgi_puts(tp, "://") == -1 ||
+	    tp_htmlescape(tp, c->server_name) == -1 ||
+	    tp_htmlescape(tp, c->document_uri) == -1)
+		return -1;
+
+	return gotweb_render_url(c, url);
+}
+
+int
 gotweb_link(struct request *c, struct gotweb_url *url, const char *fmt, ...)
 {
 	va_list ap;
@@ -2004,7 +2041,8 @@ gotweb_get_time_str(char **repo_age, time_t committer_
 	const char *hours = "hours ago",  *minutes = "minutes ago";
 	const char *seconds = "seconds ago", *now = "right now";
 	char *s;
-	char datebuf[29];
+	char datebuf[64];
+	size_t r;
 
 	*repo_age = NULL;
 
@@ -2056,6 +2094,19 @@ gotweb_get_time_str(char **repo_age, time_t committer_
 		if (asprintf(repo_age, "%s UTC", datebuf) == -1)
 			return got_error_from_errno("asprintf");
 		break;
+	case TM_RFC822:
+		if (gmtime_r(&committer_time, &tm) == NULL)
+			return got_error_from_errno("gmtime_r");
+
+		r = strftime(datebuf, sizeof(datebuf),
+		    "%a, %d %b %Y %H:%M:%S GMT", &tm);
+		if (r == 0)
+			return got_error(GOT_ERR_NO_SPACE);
+
+		*repo_age = strdup(datebuf);
+		if (*repo_age == NULL)
+			return got_error_from_errno("asprintf");
+		break;
 	}
 	return NULL;
 }
blob - fc3b18bef37416ee4320885e46d8707a3291f6c2
file + gotwebd/gotwebd.h
--- gotwebd/gotwebd.h
+++ gotwebd/gotwebd.h
@@ -52,7 +52,7 @@
 
 /* GOTWEB DEFAULTS */
 #define MAX_QUERYSTRING		 2048
-#define MAX_SCRIPT_NAME		 255
+#define MAX_DOCUMENT_URI	 255
 #define MAX_SERVER_NAME		 255
 
 #define GOTWEB_GIT_DIR		 ".git"
@@ -224,8 +224,9 @@ struct request {
 
 	char				 querystring[MAX_QUERYSTRING];
 	char				 http_host[GOTWEBD_MAXTEXT];
-	char				 script_name[MAX_SCRIPT_NAME];
+	char				 document_uri[MAX_DOCUMENT_URI];
 	char				 server_name[MAX_SERVER_NAME];
+	int				 https;
 
 	uint8_t				 request_started;
 };
@@ -412,12 +413,14 @@ enum query_actions {
 	TAG,
 	TAGS,
 	TREE,
+	RSS,
 	ACTIONS__MAX,
 };
 
 enum gotweb_ref_tm {
 	TM_DIFF,
 	TM_LONG,
+	TM_RFC822,
 };
 
 extern struct gotwebd	*gotwebd_env;
@@ -441,6 +444,7 @@ int gotweb_link(struct request *, struct gotweb_url *,
 const struct got_error *gotweb_escape_html(char **, const char *);
 const char *gotweb_action_name(int);
 int gotweb_render_url(struct request *, struct gotweb_url *);
+int gotweb_render_absolute_url(struct request *, struct gotweb_url *);
 int gotweb_link(struct request *, struct gotweb_url *, const char *, ...)
 	__attribute__((__format__(printf, 3, 4)))
 	__attribute__((__nonnull__(3)));
@@ -457,6 +461,7 @@ int	gotweb_render_commits(struct template *);
 int	gotweb_render_briefs(struct template *);
 int	gotweb_render_navs(struct template *);
 int	gotweb_render_commits(struct template *);
+int	gotweb_render_rss(struct template *);
 
 /* parse.y */
 int parse_config(const char *, struct gotwebd *);
blob - 6aca4b90f64838bba5e10738d6af4e2a01509ebf
file + gotwebd/pages.tmpl
--- gotwebd/pages.tmpl
+++ gotwebd/pages.tmpl
@@ -19,6 +19,7 @@
 #include <sys/types.h>
 #include <sys/queue.h>
 
+#include <ctype.h>
 #include <event.h>
 #include <stdint.h>
 #include <stdlib.h>
@@ -30,6 +31,9 @@ static int
 #include "gotwebd.h"
 #include "tmpl.h"
 
+static inline int rss_tag_item(struct template *, struct repo_tag *);
+static inline int rss_author(struct template *, char *);
+
 static int
 gotweb_render_age(struct template *tp, time_t time, int ref_tm)
 {
@@ -53,7 +57,7 @@ gotweb_render_age(struct template *tp, time_t time, in
 	struct server		*srv = c->srv;
 	struct querystring	*qs = c->t->qs;
 	struct gotweb_url	 u_path;
-	const char		*prfx = c->script_name;
+	const char		*prfx = c->document_uri;
 	const char		*css = srv->custom_css;
 
 	memset(&u_path, 0, sizeof(u_path));
@@ -179,6 +183,11 @@ gotweb_render_age(struct template *tp, time_t time, in
 		.index_page = -1,
 		.page = -1,
 		.path = repo_dir->name,
+	}, rss = {
+		.action = RSS,
+		.index_page = -1,
+		.page = -1,
+		.path = repo_dir->name,
 	};
 !}
 <div class="index_wrapper">
@@ -211,6 +220,8 @@ gotweb_render_age(struct template *tp, time_t time, in
       <a href="{{ render gotweb_render_url(tp->tp_arg, &tags) }}">tags</a>
       {{ " | " }}
       <a href="{{ render gotweb_render_url(tp->tp_arg, &tree) }}">tree</a>
+      {{ " | " }}
+      <a href="{{ render gotweb_render_url(tp->tp_arg, &rss) }}">rss</a>
     </div>
     <div class="dotted_line"></div>
   </div>
@@ -389,3 +400,98 @@ gotweb_render_age(struct template *tp, time_t time, in
   {{ end }}
 </div>
 {{ end }}
+
+{{ define gotweb_render_rss(struct template *tp) }}
+{!
+	struct request		*c = tp->tp_arg;
+	struct server		*srv = c->srv;
+	struct transport	*t = c->t;
+	struct repo_dir		*repo_dir = t->repo_dir;
+	struct repo_tag		*rt;
+	struct gotweb_url	 summary = {
+		.action = SUMMARY,
+		.index_page = -1,
+		.page = -1,
+		.path = repo_dir->name,
+	};
+!}
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
+  <channel>
+    <title>Tags of {{ repo_dir->name }}</title>
+    <link>
+      <![CDATA[
+        {{ render gotweb_render_absolute_url(c, &summary) }}
+      ]]>
+    </link>
+    {{ if srv->show_repo_description }}
+      <description>{{ repo_dir->description }}</description>
+    {{ end }}
+    {{ tailq-foreach rt &t->repo_tags entry }}
+      {{ render rss_tag_item(tp, rt) }}
+    {{ end }}
+  </channel>
+</rss>
+{{ end }}
+
+{{ define rss_tag_item(struct template *tp, struct repo_tag *rt) }}
+{!
+	struct request		*c = tp->tp_arg;
+	struct transport	*t = c->t;
+	struct repo_dir		*repo_dir = t->repo_dir;
+	char			*tag_name = rt->tag_name;
+	struct gotweb_url	 tag = {
+		.action = TAG,
+		.index_page = -1,
+		.page = -1,
+		.path = repo_dir->name,
+		.commit = rt->commit_id,
+	};
+
+	if (strncmp(tag_name, "refs/tags/", 10) == 0)
+		tag_name += 10;
+!}
+<item>
+  <title>{{ repo_dir->name }} {{" "}} {{ tag_name }}</title>
+  <link>
+    <![CDATA[
+      {{ render gotweb_render_absolute_url(c, &tag) }}
+    ]]>
+  </link>
+  <description>
+    <![CDATA[<pre>{{ rt->tag_commit }}</pre>]]>
+  </description>
+  {{ render rss_author(tp, rt->tagger) }}
+  <guid isPermaLink="false">{{ rt->commit_id }}</guid>
+  <pubDate>
+    {{ render gotweb_render_age(tp, rt->tagger_time, TM_RFC822) }}
+  </pubDate>
+</item>
+{{ end }}
+
+{{ define rss_author(struct template *tp, char *author) }}
+{!
+	char	*t, *mail;
+
+	/* what to do if the author name contains a paren? */
+	if (strchr(author, '(') != NULL || strchr(author, ')') != NULL)
+		return 0;
+
+	t = strchr(author, '<');
+	if (t == NULL)
+		return 0;
+	*t = '\0';
+	mail = t+1;
+
+	while (isspace((unsigned char)*--t))
+		*t = '\0';
+
+	t = strchr(mail, '>');
+	if (t == NULL)
+		return 0;
+	*t = '\0';
+!}
+<author>
+  {{ mail }} {{" "}} ({{ author }})
+</author>
+{{ end }}