From 80767bc9732bf6716697198e53ff2cb8d4ae96be Mon Sep 17 00:00:00 2001 From: Bryce Kwon Date: Wed, 12 Aug 2026 18:23:17 -1000 Subject: Restyle the sources and fix the audit's findings --- source/ui-shared.c | 1525 +++++++++++++++++++++++++++------------------------- 1 file changed, 780 insertions(+), 745 deletions(-) (limited to 'source/ui-shared.c') diff --git a/source/ui-shared.c b/source/ui-shared.c index 23da286..938f292 100644 --- a/source/ui-shared.c +++ b/source/ui-shared.c @@ -1,119 +1,69 @@ -/* ui-shared.c: common web output functions - * - * Copyright (C) 2006-2017 cgit Development Team - * - * Licensed under GNU General Public License v2 - * (see LICENSE.txt for full license text) +/* + * The output every cgit page has in common. A link from one page to another is + * built here because only this file knows whether a site addresses a request + * as a path below a virtual root or as a query string, and the two are spelled + * differently down to the separator between arguments. The headers, the + * document head, the header block, the footer, the error page and the age + * column are here so that a page handler is left with only its own body to + * produce. */ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" -#include "ui-shared.h" -#include "cmd.h" #include "html.h" -#include "version.h" - -static const char cgit_doctype[] = -"\n"; - -static char *http_date(time_t t) -{ - static char day[][4] = - {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; - static char month[][4] = - {"Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; - struct tm tm; - gmtime_r(&t, &tm); - return cgit_fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm.tm_wday], - tm.tm_mday, month[tm.tm_mon], 1900 + tm.tm_year, - tm.tm_hour, tm.tm_min, tm.tm_sec); -} +#include "shared.h" +#include "ui-shared.h" +#include "ui-snapshot.h" -void cgit_print_error(const char *fmt, ...) -{ - va_list ap; - va_start(ap, fmt); - cgit_vprint_error(fmt, ap); - va_end(ap); -} +#define DEFAULT_HTTP_PORT 80 -void cgit_vprint_error(const char *fmt, va_list ap) -{ - va_list cp; - html("
"); - va_copy(cp, ap); - html_vtxtf(fmt, cp); - va_end(cp); - html("
\n"); -} +#define SHORT_HASH_LEN 10 -const char *cgit_httpscheme(void) -{ - if (ctx.env.https && !strcmp(ctx.env.https, "on")) - return "https://"; - else - return "http://"; -} +// A max-msg-len below this leaves too little of a commit subject to be worth +// truncating, and the ellipsis is paid for out of that same budget. +#define MIN_TRUNCATE_LEN 15 +#define ELLIPSIS_LEN 3 -char *cgit_hosturl(void) -{ - if (ctx.env.http_host) - return xstrdup(ctx.env.http_host); - if (!ctx.env.server_name) - return NULL; - if (!ctx.env.server_port || atoi(ctx.env.server_port) == 80) - return xstrdup(ctx.env.server_name); - return cgit_fmtalloc("%s:%s", ctx.env.server_name, ctx.env.server_port); -} +// Lines of context git itself defaults to, which a link has no reason to name. +#define DEFAULT_DIFF_CONTEXT 3 -char *cgit_currenturl(void) -{ - const char *root = cgit_rooturl(); +// Bounds the breadcrumbs, so that one request cannot turn into an unbounded +// row of links. +#define MAX_CRUMB_LEVELS 15 - if (!ctx.qry.url) - return xstrdup(root); - if (root[0] && root[strlen(root) - 1] == '/') - return cgit_fmtalloc("%s%s", root, ctx.qry.url); - return cgit_fmtalloc("%s/%s", root, ctx.qry.url); -} +// cgit_fmt exits the request rather than truncate, so a path is only worked +// into a tab title while it still leaves room to spare in that buffer. +#define MAX_TITLE_PATH_LEN 256 -char *cgit_currentfullurl(void) +static const char *repo_basename(const char *reponame) { - const char *root = cgit_rooturl(); - const char *orig_query = ctx.env.query_string ? ctx.env.query_string : ""; - size_t len = strlen(orig_query); - char *query = xmalloc(len + 2), *start_url, *ret; - - /* Remove all url=... parts from query string */ - memcpy(query + 1, orig_query, len + 1); - query[0] = '?'; - start_url = query; - while ((start_url = strstr(start_url, "url=")) != NULL) { - if (start_url[-1] == '?' || start_url[-1] == '&') { - const char *end_url = strchr(start_url, '&'); - if (end_url) - memmove(start_url, end_url + 1, strlen(end_url)); - else - start_url[0] = '\0'; - } else - ++start_url; + // The result lives in a static buffer, so it is only good until the + // next call. + static char buf[1024]; + const char *slash; + size_t len; + int last; + + len = strlcpy(buf, reponame, sizeof(buf)); + if (len >= sizeof(buf)) + die("repo_basename: truncated repository name '%s'", reponame); + last = len - 1; + while (last && buf[last] == '/') + buf[last--] = '\0'; + if (last >= 3 && starts_with(&buf[last - 3], ".git")) { + last -= 3; + buf[last--] = '\0'; } - if (!query[1]) - query[0] = '\0'; - - if (!ctx.qry.url) - ret = cgit_fmtalloc("%s%s", root, query); - else if (root[0] && root[strlen(root) - 1] == '/') - ret = cgit_fmtalloc("%s%s%s", root, ctx.qry.url, query); - else - ret = cgit_fmtalloc("%s/%s%s", root, ctx.qry.url, query); - free(query); - return ret; + while (last && buf[last] == '/') + buf[last--] = '\0'; + slash = strrchr(buf, '/'); + if (slash) + return ++slash; + return buf; } -const char *cgit_rooturl(void) +static const char *root_url(void) { if (ctx.cfg.virtual_root) return ctx.cfg.virtual_root; @@ -121,92 +71,13 @@ const char *cgit_rooturl(void) return ctx.cfg.script_name; } -const char *cgit_loginurl(void) -{ - static const char *login_url; - if (!login_url) - login_url = cgit_fmtalloc("%s?p=login", cgit_rooturl()); - return login_url; -} - -char *cgit_repourl(const char *reponame) -{ - if (ctx.cfg.virtual_root) - return cgit_fmtalloc("%s%s/", ctx.cfg.virtual_root, reponame); - else - return cgit_fmtalloc("?r=%s", reponame); -} - -char *cgit_fileurl(const char *reponame, const char *pagename, - const char *filename, const char *query) -{ - struct strbuf sb = STRBUF_INIT; - const char *delim; - - if (ctx.cfg.virtual_root) { - strbuf_addf(&sb, "%s%s/%s/%s", ctx.cfg.virtual_root, reponame, - pagename, (filename ? filename:"")); - delim = "?"; - } else { - strbuf_addf(&sb, "?url=%s/%s/%s", reponame, pagename, - (filename ? filename : "")); - delim = "&"; - } - if (query) - strbuf_addf(&sb, "%s%s", delim, query); - return strbuf_detach(&sb, NULL); -} - -char *cgit_pageurl(const char *reponame, const char *pagename, - const char *query) -{ - return cgit_fileurl(reponame, pagename, NULL, query); -} - -const char *cgit_repobasename(const char *reponame) -{ - /* I assume we don't need to store more than one repo basename */ - static char rvbuf[1024]; - int p; - const char *rv; - size_t len; - - len = strlcpy(rvbuf, reponame, sizeof(rvbuf)); - if (len >= sizeof(rvbuf)) - die("cgit_repobasename: truncated repository name '%s'", reponame); - p = len - 1; - /* strip trailing slashes */ - while (p && rvbuf[p] == '/') - rvbuf[p--] = '\0'; - /* strip trailing .git */ - if (p >= 3 && starts_with(&rvbuf[p-3], ".git")) { - p -= 3; - rvbuf[p--] = '\0'; - } - /* strip more trailing slashes if any */ - while (p && rvbuf[p] == '/') - rvbuf[p--] = '\0'; - /* find last slash in the remaining string */ - rv = strrchr(rvbuf, '/'); - if (rv) - return ++rv; - return rvbuf; -} - -const char *cgit_snapshot_prefix(const struct cgit_repo *repo) -{ - if (repo->snapshot_prefix) - return repo->snapshot_prefix; - - return cgit_repobasename(repo->url); -} - -static void site_url(const char *page, const char *search, const char *sort, int ofs, int always_root) +static void site_url(const char *page, const char *search, const char *sort, + int ofs, int always_root) { const char *delim = "?"; if (always_root || page) - html_attr(cgit_rooturl()); + html_attr(root_url()); else { char *currenturl = cgit_currenturl(); html_attr(currenturl); @@ -236,7 +107,8 @@ static void site_url(const char *page, const char *search, const char *sort, int } static void site_link(const char *page, const char *name, const char *title, - const char *class, const char *search, const char *sort, int ofs, int always_root) + const char *class, const char *search, const char *sort, + int ofs, int always_root) { html(""); } -void cgit_index_link(const char *name, const char *title, const char *class, - const char *pattern, const char *sort, int ofs, int always_root) -{ - site_link(NULL, name, title, class, pattern, sort, ofs, always_root); -} - -static const char *repolink(const char *title, const char *class, const char *page, - const char *head, const char *path) +/* + * Open an anchor and write the part of its href naming the repository, the + * page and the path, leaving the href quote open for the caller to add its own + * arguments and close. The return is the delimiter to put before the first of + * those, which is still a question mark unless a query string has been opened. + */ +static const char *repolink(const char *title, const char *class, + const char *page, const char *head, + const char *path) { const char *delim = "?"; @@ -313,114 +186,38 @@ static const char *repolink(const char *title, const char *class, const char *pa return cgit_fmt("%s", delim); } -static void reporevlink(const char *page, const char *name, const char *title, - const char *class, const char *head, const char *rev, - const char *path) +static const char *emit_rev_arg(const char *delim, const char *rev) { - const char *delim; - - delim = repolink(title, class, page, head, path); - if (rev && ctx.qry.head != NULL && strcmp(rev, ctx.qry.head)) { + if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) { html(delim); html("id="); html_url_arg(rev); + return "&"; } - html("'>"); - html_txt(name); - html(""); -} - -void cgit_summary_link(const char *name, const char *title, const char *class, - const char *head) -{ - reporevlink(NULL, name, title, class, head, NULL, NULL); -} - -void cgit_tag_link(const char *name, const char *title, const char *class, - const char *tag) -{ - reporevlink("tag", name, title, class, tag, NULL, NULL); -} - -void cgit_tree_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path) -{ - reporevlink("tree", name, title, class, head, rev, path); -} - -void cgit_plain_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path) -{ - reporevlink("plain", name, title, class, head, rev, path); -} - -void cgit_blame_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path) -{ - reporevlink("blame", name, title, class, head, rev, path); + return delim; } -void cgit_log_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path, - int ofs, const char *grep, const char *pattern, int showmsg, - int follow) +static void reporevlink(const char *page, const char *name, const char *title, + const char *class, const char *head, const char *rev, + const char *path) { const char *delim; - delim = repolink(title, class, "log", head, path); - if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) { - html(delim); - html("id="); - html_url_arg(rev); - delim = "&"; - } - if (grep && pattern) { - html(delim); - html("qt="); - html_url_arg(grep); - delim = "&"; - html(delim); - html("q="); - html_url_arg(pattern); - } - if (ofs > 0) { - html(delim); - html("ofs="); - htmlf("%d", ofs); - delim = "&"; - } - if (showmsg) { - html(delim); - html("showmsg=1"); - delim = "&"; - } - if (follow) { - html(delim); - html("follow=1"); - } + delim = repolink(title, class, page, head, path); + emit_rev_arg(delim, rev); html("'>"); html_txt(name); html(""); } -void cgit_commit_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path) +static void emit_diff_args(const char *delim) { - const char *delim; - - delim = repolink(title, class, "commit", head, path); - if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) { - html(delim); - html("id="); - html_url_arg(rev); - delim = "&"; - } if (ctx.qry.difftype) { html(delim); htmlf("dt=%d", ctx.qry.difftype); delim = "&"; } - if (ctx.qry.context > 0 && ctx.qry.context != 3) { + if (ctx.qry.context > 0 && ctx.qry.context != DEFAULT_DIFF_CONTEXT) { html(delim); html("context="); htmlf("%d", ctx.qry.context); @@ -435,89 +232,370 @@ void cgit_commit_link(const char *name, const char *title, const char *class, html(delim); html("follow=1"); } - html("'>"); - if (name && name[0] != '\0') { - if (ctx.cfg.max_msg_len >= 15 && - strlen(name) > (size_t)ctx.cfg.max_msg_len) { - html_ntxt(name, ctx.cfg.max_msg_len - 3); - html("..."); - } else - html_txt(name); - } else - html_txt("(no commit message)"); - html(""); } -void cgit_refs_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path) +static struct string_list_item *lookup_path(struct string_list *list, + const char *path) { - reporevlink("refs", name, title, class, head, rev, path); -} + struct string_list_item *item; -void cgit_snapshot_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, - const char *archivename) -{ - reporevlink("snapshot", name, title, class, head, rev, archivename); + while (path && path[0]) { + if ((item = string_list_lookup(list, path))) + return item; + if (!(path = strchr(path, '/'))) + break; + path++; + } + return NULL; } -void cgit_diff_link(const char *name, const char *title, const char *class, - const char *head, const char *new_rev, const char *old_rev, - const char *path) +/* + * A repository can supply the module-link template through a repo-local + * cgitrc, so the %s placeholders are expanded here instead of the template + * being handed to printf, where a surplus conversion would read past the + * argument list. + */ +static void emit_module_link(const char *tmpl, const char **args, int nargs) { - const char *delim; + struct strbuf sb = STRBUF_INIT; + int used = 0; - delim = repolink(title, class, "diff", head, path); - if (new_rev && ctx.qry.head != NULL && strcmp(new_rev, ctx.qry.head)) { - html(delim); - html("id="); - html_url_arg(new_rev); - delim = "&"; + while (*tmpl) { + if (*tmpl != '%') { + strbuf_addch(&sb, *tmpl++); + continue; + } + tmpl++; + if (*tmpl == '%') { + strbuf_addch(&sb, '%'); + tmpl++; + } else if (*tmpl == 's' && used < nargs) { + strbuf_addstr(&sb, args[used++]); + tmpl++; + } else { + strbuf_addch(&sb, '%'); + } } - if (old_rev) { - html(delim); - html("id2="); - html_url_arg(old_rev); - delim = "&"; + html_attr(sb.buf); + strbuf_release(&sb); +} + +static struct date_mode configured_date_mode(void) +{ + struct date_mode mode = ctx.cfg.date_mode; + + if (ctx.cfg.local_time) + mode.local = 1; + return mode; +} + +static void print_abs_date(time_t t, int tz) +{ + html(""); + html_txt(show_date(t, tz, configured_date_mode())); + html(""); +} + +static void print_rel_date(time_t t, int tz, double count, const char *class, + const char *suffix) +{ + htmlf("%.0f %s", count, suffix); +} + +static char *http_date(time_t t) +{ + static char day[][4] = + {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + static char month[][4] = + {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + struct tm tm; + gmtime_r(&t, &tm); + return cgit_fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm.tm_wday], + tm.tm_mday, month[tm.tm_mon], 1900 + tm.tm_year, + tm.tm_hour, tm.tm_min, tm.tm_sec); +} + +static void print_rel_vcs_link(const char *url) +{ + html("\n"); +} + +static int emit_css_link(struct string_list_item *item, void *fallback) +{ + // An empty css entry is how a site turns the stylesheet off. + if (item && *item->string == '\0') + return 0; + + html("\n"); + + return 0; +} + +static int emit_js_link(struct string_list_item *item, void *fallback) +{ + // An empty js entry is how a site turns the script off. + if (item && *item->string == '\0') + return 0; + + html("\n"); + + return 0; +} + +static void add_clone_urls(void (*fn)(const char *), char *urls, char *suffix) +{ + struct strbuf **url_list = strbuf_split_str(urls, ' ', 0); + int i; + + for (i = 0; url_list[i]; i++) { + strbuf_rtrim(url_list[i]); + if (url_list[i]->len == 0) + continue; + if (suffix && *suffix) + strbuf_addf(url_list[i], "/%s", suffix); + fn(url_list[i]->buf); } - if (ctx.qry.difftype) { - html(delim); - htmlf("dt=%d", ctx.qry.difftype); - delim = "&"; + + strbuf_list_free(url_list); +} + +static int print_branch_option(const struct reference *ref, void *counter) +{ + int *count = counter; + const char *name = ref->name; + // The switcher runs on every page, so it is bounded like the refs list. + if (ctx.cfg.max_ref_count && *count >= ctx.cfg.max_ref_count) + return -1; + (*count)++; + html_option(name, name, ctx.qry.head); + return 0; +} + +static void print_header(void) +{ + const char *logo = NULL, *logo_link = NULL; + + if (ctx.repo && ctx.repo->logo && *ctx.repo->logo) + logo = ctx.repo->logo; + else + logo = ctx.cfg.logo; + if (ctx.repo && ctx.repo->logo_link && *ctx.repo->logo_link) + logo_link = ctx.repo->logo_link; + else + logo_link = ctx.cfg.logo_link; + + html("\n"); +} + +static const char *tab_class(const char *page) +{ + if (!ctx.qry.page) + return NULL; + + return strcmp(ctx.qry.page, page) ? NULL : "active"; +} + +static const char *tab_title(const char *format, const char *fallback, + const char *path) +{ + if (!path || !*path || strlen(path) > MAX_TITLE_PATH_LEN) + return fallback; + return cgit_fmt(format, path); +} + +static void print_repo_tabs(void) +{ + const char *vpath = ctx.qry.vpath; + + html("
    \n"); + if (ctx.repo->readme.nr) { + html("
  • "); + reporevlink("about", "about", "About this repository", + tab_class("about"), ctx.qry.head, NULL, NULL); + html("
  • \n"); } - html("'>"); - html_txt(name); - html(""); + html("
  • "); + cgit_summary_link("summary", "Repository summary", tab_class("summary"), + ctx.qry.head); + html("
  • \n
  • "); + cgit_refs_link("refs", "Branches and tags", tab_class("refs"), + ctx.qry.head, ctx.qry.oid, NULL); + html("
  • \n
  • "); + cgit_log_link("log", + tab_title("Commit history of %s", "Commit history", vpath), + tab_class("log"), ctx.qry.head, + NULL, ctx.qry.vpath, 0, NULL, NULL, + ctx.qry.showmsg, ctx.qry.follow); + html("
  • \n
  • "); + if (ctx.qry.page && !strcmp(ctx.qry.page, "blame")) + cgit_blame_link("blame", + tab_title("Line-by-line blame of %s", "Blame", vpath), + tab_class("blame"), ctx.qry.head, + ctx.qry.oid, ctx.qry.vpath); + else + cgit_tree_link("tree", + tab_title("Browse the tree at %s", "Browse the file tree", vpath), + tab_class("tree"), ctx.qry.head, + ctx.qry.oid, ctx.qry.vpath); + html("
  • \n
  • "); + cgit_commit_link("commit", "The current commit", tab_class("commit"), + ctx.qry.head, ctx.qry.oid, ctx.qry.vpath); + html("
  • \n
  • "); + cgit_diff_link("diff", + tab_title("Changes to %s", "Changes in the current commit", vpath), + tab_class("diff"), ctx.qry.head, + ctx.qry.oid, ctx.qry.oid2, ctx.qry.vpath); + html("
  • \n"); + if (ctx.repo->enable_stats) { + html("
  • "); + cgit_stats_link("stats", "Commit activity", tab_class("stats"), + ctx.qry.head, ctx.qry.vpath); + html("
  • \n"); + } + if (ctx.repo->homepage) { + html("
  • homepage
  • \n"); + } + html("
\n"); } -void cgit_patch_link(const char *name, const char *title, const char *class, - const char *head, const char *rev, const char *path) +static void print_repo_search(void) { - reporevlink("patch", name, title, class, head, rev, path); + html("\n"); } -void cgit_stats_link(const char *name, const char *title, const char *class, - const char *head, const char *path) +static void print_site_tabs(void) { - reporevlink("stats", name, title, class, head, NULL, path); + html("
    \n
  • "); + site_link(NULL, "index", "Repository index", tab_class("repolist"), + NULL, NULL, 0, 1); + html("
  • \n"); + if (ctx.cfg.root_readme) { + html("
  • "); + site_link("about", "about", "About this site", + tab_class("about"), NULL, NULL, 0, 1); + html("
  • \n"); + } + html("
\n"); } -static void cgit_self_link(char *name, const char *title, const char *class) +static void print_site_search(void) +{ + char *currenturl = cgit_currenturl(); + + html("\n"); + free(currenturl); +} + +// The link is built out of the request in ctx.qry, so a caller that alters a +// field of ctx.qry first gets a link differing in exactly that. +static void snapshot_link(const char *name, const char *title, const char *class, + const char *head, const char *rev, + const char *archivename) +{ + reporevlink("snapshot", name, title, class, head, rev, archivename); +} + +static void self_link(const char *name, const char *title, const char *class) { if (!strcmp(ctx.qry.page, "repolist")) cgit_index_link(name, title, class, ctx.qry.search, ctx.qry.sort, @@ -558,7 +636,7 @@ static void cgit_self_link(char *name, const char *title, const char *class) ctx.qry.has_oid ? ctx.qry.oid : NULL, ctx.qry.path); else if (!strcmp(ctx.qry.page, "snapshot")) - cgit_snapshot_link(name, title, class, ctx.qry.head, + snapshot_link(name, title, class, ctx.qry.head, ctx.qry.has_oid ? ctx.qry.oid : NULL, ctx.qry.path); else if (!strcmp(ctx.qry.page, "diff")) @@ -569,7 +647,6 @@ static void cgit_self_link(char *name, const char *title, const char *class) cgit_stats_link(name, title, class, ctx.qry.head, ctx.qry.path); else { - /* Don't known how to make link for this page */ repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path); html(">