/* * 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" // Stands in for every byte the feed cannot carry. #define XML_REPLACEMENT "?" /* * 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)); } /* * The XML counterpart of html_txt. A browser shrugs at a stray control byte * or broken UTF-8, but 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 = cgit_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, *pageurl; char delim = '&'; 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.enable_plain_email) print_email(info->author_email); html("\n"); html(""); xml_txt(feed_date(info->author_date)); html("\n"); 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, *fullurl, *repourl; // 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; struct strbuf idbuf = STRBUF_INIT; 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); // A failed setup leaves the walk holding freed commits, so it must // not be read from. if (prepare_revision_walk(&rev)) { cgit_print_error_page(500, "Internal Server Error", "Unable to read the history"); return; } // CGI guarantees a server name, so only a bare test run reaches the // fallback, which keeps the mandatory feed id and links present. host = cgit_hosturl(); if (!host) host = xstrdup("localhost"); ctx.page.mimetype = "application/atom+xml"; ctx.page.charset = PAGE_ENCODING; 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"); fullurl = cgit_currentfullurl(); repourl = cgit_repourl(ctx.repo->url); 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))) { 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, so the epoch stands in. html(""); xml_txt(feed_date(0)); html("\n"); } html("\n"); free(host); }