diff --git a/gotwebd/files/htdocs/gotwebd/gotweb.css b/gotwebd/files/htdocs/gotwebd/gotweb.css index de0fc45..4b0ae1d 100755 --- a/gotwebd/files/htdocs/gotwebd/gotweb.css +++ b/gotwebd/files/htdocs/gotwebd/gotweb.css @@ -382,6 +382,38 @@ header.subtitle h2 { margin-bottom: 20px; border-collapse: collapse; } +.readme { + max-width: 900px; + line-height: 1.5; + padding: 1em 0; +} +.readme h1, .readme h2, .readme h3 { + border-bottom: 1px solid LightSlateGray; + padding-bottom: 0.2em; +} +.readme pre { + background-color: #f6f6f6; + padding: 0.5em; + overflow-x: auto; +} +.readme code { + font-family: monospace; + background-color: #f6f6f6; + padding: 0.1em 0.3em; +} +.readme table { + border-collapse: collapse; +} +.readme th, .readme td { + border: 1px solid LightSlateGray; + padding: 0.3em 0.6em; +} +.readme blockquote { + border-left: 3px solid LightSlateGray; + margin-left: 0; + padding-left: 1em; + color: #555; +} .tree_wrapper:nth-child(odd) { background-color: #d8f3ef; } diff --git a/gotwebd/got_operations.c b/gotwebd/got_operations.c index 68e9ef0..5f0ac3a 100644 --- a/gotwebd/got_operations.c +++ b/gotwebd/got_operations.c @@ -28,6 +28,8 @@ #include #include +#include + #include "got_error.h" #include "got_object.h" #include "got_reference.h" @@ -760,6 +762,62 @@ got_output_repo_tree(struct request *c, char **readme, return 0; } +/* + * Render a Markdown README (buf/len) into a sanitized HTML5 fragment, + * the same way GitHub renders README.md on a repository's front page. + * Returns 0 and a NUL-terminated *html on success, -1 on failure. + * The caller must free *html. + */ +int +got_render_readme_markdown(char **html, const uint8_t *buf, size_t len) +{ + struct lowdown_opts opts; + char *out = NULL; + size_t outlen = 0; + + *html = NULL; + + memset(&opts, 0, sizeof(opts)); + opts.type = LOWDOWN_HTML; + opts.feat = LOWDOWN_AUTOLINK | LOWDOWN_TABLES | LOWDOWN_FENCED | + LOWDOWN_STRIKE | LOWDOWN_SUPER | LOWDOWN_COMMONMARK | + LOWDOWN_DEFLIST | LOWDOWN_ATTRS; + /* + * OWASP-sanitize any raw HTML embedded in the README: this content + * comes from repository data which gotwebd must not trust blindly. + */ + opts.oflags = LOWDOWN_HTML_HEAD_IDS | LOWDOWN_HTML_NUM_ENT | + LOWDOWN_HTML_OWASP | LOWDOWN_SMARTY; + + if (!lowdown_buf(&opts, (const char *)buf, len, &out, &outlen, NULL)) + return -1; + + /* lowdown_buf() does not guarantee NUL-termination. */ + *html = malloc(outlen + 1); + if (*html == NULL) { + free(out); + return -1; + } + memcpy(*html, out, outlen); + (*html)[outlen] = '\0'; + free(out); + return 0; +} + +/* + * Return non-zero if the given README file name should be rendered as + * Markdown (case-insensitive ".md" suffix), matching GitHub's convention. + */ +int +got_readme_is_markdown(const char *name) +{ + size_t len = strlen(name); + + if (len < 3) + return 0; + return strcasecmp(name + len - 3, ".md") == 0; +} + const struct got_error * got_open_blob_for_output(struct got_blob_object **blob, int *fd, int *binary, struct request *c, const char *directory, const char *file, diff --git a/gotwebd/gotwebd.h b/gotwebd/gotwebd.h index 09387b7..adcf4ba 100644 --- a/gotwebd/gotwebd.h +++ b/gotwebd/gotwebd.h @@ -703,6 +703,8 @@ const struct got_error *got_get_repo_heads(struct request *); const struct got_error *got_open_diff_for_output(FILE **, struct request *); int got_output_repo_tree(struct request *, char **, int (*)(struct template *, struct got_tree_entry *)); +int got_render_readme_markdown(char **, const uint8_t *, size_t); +int got_readme_is_markdown(const char *); const struct got_error *got_open_blob_for_output(struct got_blob_object **, int *, int *, struct request *, const char *, const char *, const char *); int got_output_blob_by_lines(struct template *, struct got_blob_object *, diff --git a/gotwebd/pages.tmpl b/gotwebd/pages.tmpl index a148101..26560ac 100644 --- a/gotwebd/pages.tmpl +++ b/gotwebd/pages.tmpl @@ -737,7 +737,11 @@ nextsep(char *s, char **t) const struct querystring *qs = c->t->qs; struct gotweb_url url; char *readme = NULL; + char *readme_buf = NULL; + char *readme_html = NULL; + size_t readme_bufsz = 0; int binary; + int is_markdown = 0; const uint8_t *buf; size_t len; !} @@ -746,6 +750,8 @@ nextsep(char *s, char **t) {{ if readme }} {! + is_markdown = got_readme_is_markdown(readme); + error = got_open_blob_for_output(&t->blob, &t->fd, &binary, c, qs->folder[0] ? qs->folder : NULL, readme, qs->commit[0] ? qs->commit : NULL); @@ -768,6 +774,47 @@ nextsep(char *s, char **t) {{ readme }} + {{ if is_markdown }} + {! + /* Slurp the blob so lowdown can parse it as one buffer. */ + for (;;) { + char *tmp; + + error = got_object_blob_read_block(&len, t->blob); + if (error) { + free(readme); + free(readme_buf); + return (-1); + } + if (len == 0) + break; + buf = got_object_blob_get_read_buf(t->blob); + tmp = realloc(readme_buf, readme_bufsz + len); + if (tmp == NULL) { + free(readme); + free(readme_buf); + return (-1); + } + readme_buf = tmp; + memcpy(readme_buf + readme_bufsz, buf, len); + readme_bufsz += len; + } + if (got_render_readme_markdown(&readme_html, + (uint8_t *)readme_buf, readme_bufsz) == -1) { + free(readme); + free(readme_buf); + return (-1); + } + !} +
+ {! if (tp_write(tp, readme_html, strlen(readme_html)) == -1) { + free(readme); + free(readme_buf); + free(readme_html); + return (-1); + } !} +
+ {{ else }}
         {!
 		for (;;) {
@@ -786,10 +833,15 @@ nextsep(char *s, char **t)
 		}
         !}
       
+ {{ end }} {{ end }} {{ end }} {{ finally }} - {! free(readme); !} + {! + free(readme); + free(readme_buf); + free(readme_html); + !} {{ end }} {{ define gotweb_render_tree(struct template *tp) }} Index: gotwebd/Makefile =================================================================== --- gotwebd/Makefile +++ gotwebd/Makefile @@ -30,7 +30,7 @@ MAN = ${PROG}.conf.5 ${PROG}.8 CPPFLAGS += -I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR} CPPFLAGS += -I${.CURDIR}/../template -LDADD += -lz -levent -lutil -lm -lcrypto +LDADD += -lz -levent -lutil -lm -lcrypto -llowdown YFLAGS = DPADD = ${LIBEVENT} ${LIBUTIL} ${LIBM} ${LIBCRYPTO} #CFLAGS += -DGOT_NO_OBJ_CACHE