From: Omar Polo Subject: gotwebd: rss feeds for repo tags To: gameoftrees@openbsd.org Date: Mon, 19 Dec 2022 12:45:28 +0100 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 #include +#include #include #include #include @@ -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, }; !}
@@ -211,6 +220,8 @@ gotweb_render_age(struct template *tp, time_t time, in tags {{ " | " }} tree + {{ " | " }} + rss
@@ -389,3 +400,98 @@ gotweb_render_age(struct template *tp, time_t time, in {{ end }} {{ 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, + }; +!} + + + + Tags of {{ repo_dir->name }} + + + + {{ if srv->show_repo_description }} + {{ repo_dir->description }} + {{ end }} + {{ tailq-foreach rt &t->repo_tags entry }} + {{ render rss_tag_item(tp, rt) }} + {{ end }} + + +{{ 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; +!} + + {{ repo_dir->name }} {{" "}} {{ tag_name }} + + + + + {{ rt->tag_commit }}]]> + + {{ render rss_author(tp, rt->tagger) }} + {{ rt->commit_id }} + + {{ render gotweb_render_age(tp, rt->tagger_time, TM_RFC822) }} + + +{{ 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'; +!} + + {{ mail }} {{" "}} ({{ author }}) + +{{ end }}