/*
* The Atom feed for a repository, which lists recent commits so a reader can
* follow the project from a feed reader instead of the browsable pages. The
* response is XML rather than a page, so this file writes its own HTTP headers
* and never goes through the shared HTML layout.
*/
#define USE_THE_REPOSITORY_VARIABLE
#include "cgit.h"
#include "html.h"
#include "parsing.h"
#include "shared.h"
#include "ui-atom.h"
#include "ui-shared.h"
/*
* Atom timestamps have to be RFC 3339, so a feed ignores the date-format and
* local-time settings the browsable pages honour. The zero is the timezone
* offset, which pins every feed date to UTC.
*/
static const char *feed_date(timestamp_t when)
{
return show_date(when, 0, date_mode_from_type(DATE_ISO8601_STRICT));
}
// U+FFFD, which stands in for every byte the feed cannot carry.
#define XML_REPLACEMENT "\xef\xbf\xbd"
/*
* How many bytes the UTF-8 sequence at p holds, or 0 when the bytes there are
* not a valid sequence. The lead-byte ranges fold in the overlong, surrogate
* and out-of-range cases, so a 0 is the only error signal a caller needs.
*/
static size_t utf8_seq_len(const unsigned char *p, size_t left)
{
size_t len, i;
if (p[0] >= 0xc2 && p[0] <= 0xdf)
len = 2;
else if (p[0] >= 0xe0 && p[0] <= 0xef)
len = 3;
else if (p[0] >= 0xf0 && p[0] <= 0xf4)
len = 4;
else
return 0;
if (left < len)
return 0;
for (i = 1; i < len; i++)
if ((p[i] & 0xc0) != 0x80)
return 0;
if (p[0] == 0xe0 && p[1] < 0xa0)
return 0;
if (p[0] == 0xed && p[1] > 0x9f)
return 0;
if (p[0] == 0xf0 && p[1] < 0x90)
return 0;
if (p[0] == 0xf4 && p[1] > 0x8f)
return 0;
return len;
}
/*
* The XML counterpart of html_txt. Commit metadata is arbitrary bytes, and
* where a browser shrugs at a stray control byte or a broken UTF-8 sequence,
* an XML reader must reject the whole feed, so both are replaced instead of
* passed through.
*/
static void xml_txt(const char *txt)
{
const unsigned char *p = (const unsigned char *)txt;
size_t left = txt ? strlen(txt) : 0;
struct strbuf sb = STRBUF_INIT;
while (left) {
unsigned char c = *p;
size_t seq;
if (c == '<') {
strbuf_addstr(&sb, "<");
} else if (c == '>') {
strbuf_addstr(&sb, ">");
} else if (c == '&') {
strbuf_addstr(&sb, "&");
} else if (c == '\t' || c == '\n' || c == '\r' || (c >= 0x20 && c < 0x80)) {
strbuf_addch(&sb, c);
} else if (c < 0x20) {
strbuf_addstr(&sb, XML_REPLACEMENT);
} else if ((seq = utf8_seq_len(p, left))) {
// U+FFFE and U+FFFF are valid UTF-8 but not XML.
if (seq == 3 && p[0] == 0xef && p[1] == 0xbf &&
p[2] >= 0xbe)
strbuf_addstr(&sb, XML_REPLACEMENT);
else
strbuf_add(&sb, p, seq);
p += seq - 1;
left -= seq - 1;
} else {
strbuf_addstr(&sb, XML_REPLACEMENT);
}
p++;
left--;
}
html_raw(sb.buf, sb.len);
strbuf_release(&sb);
}
/*
* cgit keeps an address with the angle brackets it was written with, while
* Atom wants the bare address.
*/
static void print_email(const char *email)
{
char *copy = xstrdup(email);
char *start, *end;
start = strchr(copy, '<');
if (start)
start++;
else
start = copy;
end = strchr(start, '>');
if (end)
*end = '\0';
html("");
xml_txt(start);
html("\n");
free(copy);
}
static void print_entry(struct commit *commit, const char *host)
{
struct commitinfo *info;
char *hex;
info = cgit_parse_commit(commit);
hex = oid_to_hex(&commit->object.oid);
html("\n");
html("");
xml_txt(info->subject);
html("\n");
html("");
xml_txt(feed_date(info->committer_date));
html("\n");
html("\n");
// A person construct must hold a name, so a nameless commit falls
// back to the address and then to a placeholder.
html("");
if (info->author)
xml_txt(info->author);
else if (info->author_email)
xml_txt(info->author_email);
else
html("unknown");
html("\n");
if (info->author_email && !ctx.cfg.noplainemail)
print_email(info->author_email);
html("\n");
html("");
xml_txt(feed_date(info->author_date));
html("\n");
{
char *pageurl;
char delim = '&';
html("\n");
free(pageurl);
}
html("");
html_txtf("urn:%s:%s", the_hash_algo->name, hex);
html("\n");
html("");
xml_txt(info->msg);
html("\n");
html("\n");
cgit_free_commitinfo(info);
}
void cgit_print_atom(char *tip, const char *path, int max_count)
{
char *host;
// setup_revisions reads a command line, so the first slot is the
// unused program name and parsing starts at the second.
const char *argv[] = {NULL, tip, NULL, NULL, NULL};
struct commit *commit;
struct rev_info rev;
int argc = 2;
bool need_updated = true;
if (ctx.qry.show_all)
argv[1] = "--all";
else if (!tip)
argv[1] = ctx.qry.head;
if (path) {
argv[argc++] = "--";
argv[argc++] = path;
}
repo_init_revisions(the_repository, &rev, NULL);
rev.abbrev = DEFAULT_ABBREV;
rev.commit_format = CMIT_FMT_DEFAULT;
rev.verbose_header = 1;
rev.show_root_diff = 0;
rev.max_count = max_count;
setup_revisions(argc, argv, &rev, NULL);
prepare_revision_walk(&rev);
// CGI guarantees a server name, so only a bare test run reaches the
// fallback, and a placeholder there keeps the mandatory feed id and
// the links present rather than dropping them.
host = cgit_hosturl();
if (!host)
host = xstrdup("localhost");
ctx.page.mimetype = "application/atom+xml";
ctx.page.charset = "utf-8";
cgit_print_http_headers();
html("\n");
html("\n");
html("");
xml_txt(ctx.repo->name);
if (path) {
html("/");
xml_txt(path);
}
if (tip && !ctx.qry.show_all) {
html(", branch ");
xml_txt(tip);
}
html("\n");
html("");
xml_txt(ctx.repo->desc);
html("\n");
{
char *fullurl = cgit_currentfullurl();
char *repourl = cgit_repourl(ctx.repo->url);
struct strbuf idbuf = STRBUF_INIT;
strbuf_addf(&idbuf, "%s%s%s", cgit_httpscheme(), host, fullurl);
html("");
xml_txt(idbuf.buf);
html("\n");
strbuf_release(&idbuf);
html("\n");
html("\n");
free(fullurl);
free(repourl);
}
while ((commit = get_revision(&rev)) != NULL) {
if (need_updated) {
html("");
xml_txt(feed_date(commit->date));
html("\n");
need_updated = false;
}
print_entry(commit, host);
// release_commit_memory frees the parent list without clearing
// the pointer to it, so drop the dangling reference here.
release_commit_memory(the_repository->parsed_objects, commit);
commit->parents = NULL;
}
if (need_updated) {
// Atom makes a feed level updated mandatory, and an empty feed
// has no commit to take one from.
html("");
xml_txt(feed_date(time(NULL)));
html("\n");
}
html("\n");
free(host);
}