diff options
context:
space:
mode:
authorBryce Kwon <bryce@brycekwon.com>
committerBryce Kwon <bryce@brycekwon.com>
commit
parent
tree
download
Replace the browser markdown renderer with a filter
The readme is now escaped plain text unless `about-filter` points at the new `about-render.lua`, which renders markdown, man pages and plain text server-side. `enable-markdown` goes away with the renderer.
Diffstat (limited to '')
-rw-r--r--README.txt2
-rw-r--r--assets/cgit.css8
-rw-r--r--assets/cgit.js164
-rw-r--r--cgitrc.5.txt19
-rw-r--r--examples/cgitrc19
-rw-r--r--extensions/about-render.lua592
-rw-r--r--source/cgit.c3
-rw-r--r--source/cgit.h1
-rw-r--r--source/ui-summary.c34
-rw-r--r--tests/t0200-security.sh4
10 files changed, 613 insertions, 233 deletions
diff --git a/README.txt b/README.txt
index d3b32ec..d385d31 100644
--- a/README.txt
+++ b/README.txt
@@ -96,6 +96,8 @@ header lists the exact install commands for its own dependencies.
`luaossl`.
* The syntax highlighter (`syntax-highlight.lua`) needs `lpeg` and a Scintillua
lexer set.
+* The about-page renderer (`about-render.lua`) needs `lpeg` for markdown and
+ man pages. Plain text needs only Lua.
These filters target Lua 5.1 through 5.4 and LuaJIT. `luaossl` has no Lua 5.5
build, so build cgit against 5.1 to 5.4 if you use the auth or email filters.
diff --git a/assets/cgit.css b/assets/cgit.css
index 3255068..ba7d7b1 100644
--- a/assets/cgit.css
+++ b/assets/cgit.css
@@ -520,14 +520,6 @@ div#cgit pre.plaintext {
margin: 0;
}
-/* Markdown source before the client-side renderer has run, and for good
- * when scripting is off. Keeps the line structure so it reads as text. */
-div#cgit .markdown[data-markdown] {
- white-space: pre-wrap;
- overflow-wrap: anywhere;
- font-family: var(--font-mono);
-}
-
div#cgit .markdown {
line-height: 1.6;
overflow-wrap: break-word;
diff --git a/assets/cgit.js b/assets/cgit.js
index 10a34cd..6fe53ba 100644
--- a/assets/cgit.js
+++ b/assets/cgit.js
@@ -194,167 +194,3 @@ document.addEventListener("DOMContentLoaded", function () {
}, false);
})();
-
-/* Built-in Markdown rendering for the about page. When no about-filter is
- * configured, cgit escapes a markdown readme into a data-markdown container
- * (see cgit_print_repo_readme) and this renders a deliberately small, safe
- * subset client-side: headings, lists, blockquotes, rules, fenced and inline
- * code, pipe tables, links, images and emphasis. Every run of text is escaped
- * before any markup is added, and link and image URLs are restricted to http,
- * https, mailto and relative targets, so a hostile readme cannot inject markup
- * or scripts. Fenced code carries its language in data-lang as a styling
- * hook. Without JavaScript the escaped source stays readable as plain text.
- *
- * This is intentionally a subset, not CommonMark: no reference links, raw HTML
- * passthrough, nested lists or setext headings. Configure an about-filter to
- * replace it, or set enable-markdown=0 to turn it off. */
-
-(function () {
-
-var MAX_BYTES = 400000;
-
-function esc(s) {
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
-}
-
-function escAttr(s) {
- return esc(s).replace(/"/g, "&quot;").replace(/'/g, "&#39;");
-}
-
-/* Return the url if its scheme is safe, else "". Whitespace and control bytes
- * are stripped before the scheme is read because browsers ignore them when
- * resolving it, so "java\nscript:..." must still be caught as javascript. */
-function safeUrl(url) {
- url = (url || "").replace(/[\u0000-\u0020]+/g, "");
- var scheme = /^([a-z][a-z0-9+.\-]*):/i.exec(url);
- if (scheme && !/^(https?|mailto)$/i.test(scheme[1]))
- return "";
- return url;
-}
-
-function link(text, url, image) {
- var u = safeUrl(url);
- if (!u)
- return image ? esc("![" + text + "]") : inline(text);
- if (image)
- return "<img src='" + escAttr(u) + "' alt='" + escAttr(text) + "'/>";
- return "<a href='" + escAttr(u) + "'>" + inline(text) + "</a>";
-}
-
-/* Inline rendering over one block of text. Scans to the next marker character
- * and bulk-escapes the plain text in between, so it stays roughly linear. */
-function inline(s) {
- var out = "", i = 0, n = s.length, marker = /[`!\[*_]/g, m, rest;
- while (i < n) {
- marker.lastIndex = i;
- m = marker.exec(s);
- if (!m) { out += esc(s.slice(i)); break; }
- if (m.index > i) { out += esc(s.slice(i, m.index)); i = m.index; }
- rest = s.slice(i);
- if ((m = /^`([^`]+)`/.exec(rest)))
- out += "<code>" + esc(m[1]) + "</code>";
- else if ((m = /^!\[([^\]]*)\]\(\s*([^)\s]+)[^)]*\)/.exec(rest)))
- out += link(m[1], m[2], true);
- else if ((m = /^\[([^\]]*)\]\(\s*([^)\s]+)[^)]*\)/.exec(rest)))
- out += link(m[1], m[2], false);
- else if ((m = /^(\*\*|__)([\s\S]+?)\1/.exec(rest)))
- out += "<strong>" + inline(m[2]) + "</strong>";
- else if ((m = /^(\*|_)([^\s][\s\S]*?)\1/.exec(rest)))
- out += "<em>" + inline(m[2]) + "</em>";
- else { out += esc(s.charAt(i)); i++; continue; }
- i += m[0].length;
- }
- return out;
-}
-
-function cells(row) {
- return row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map(function (c) {
- return c.trim();
- });
-}
-
-function render(src) {
- var lines = src.replace(/\r\n?/g, "\n").split("\n");
- var out = "", i = 0, n = lines.length, line, m, k;
- while (i < n) {
- line = lines[i];
- if (/^\s*$/.test(line)) { i++; continue; }
- if ((m = /^\s*(`{3,}|~{3,})\s*([\w.+#-]*)/.exec(line))) {
- var fence = m[1].charAt(0) === "`" ? /^\s*`{3,}\s*$/ : /^\s*~{3,}\s*$/;
- var lang = m[2], code = "";
- for (i++; i < n && !fence.test(lines[i]); i++)
- code += lines[i] + "\n";
- i++;
- out += "<pre><code" + (lang ? " data-lang='" + escAttr(lang) + "'" : "") +
- ">" + esc(code) + "</code></pre>";
- continue;
- }
- if ((m = /^(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line))) {
- k = m[1].length;
- out += "<h" + k + ">" + inline(m[2]) + "</h" + k + ">";
- i++; continue;
- }
- if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) { out += "<hr/>"; i++; continue; }
- if (/^\s*>/.test(line)) {
- var q = "";
- for (; i < n && /^\s*>/.test(lines[i]); i++)
- q += lines[i].replace(/^\s*>\s?/, "") + "\n";
- out += "<blockquote>" + render(q) + "</blockquote>";
- continue;
- }
- if (line.indexOf("|") >= 0 && i + 1 < n &&
- /^\s*\|?(\s*:?-+:?\s*\|)+\s*:?-+:?\s*\|?\s*$/.test(lines[i + 1])) {
- var head = cells(line), t = "<table><thead><tr>";
- for (k = 0; k < head.length; k++)
- t += "<th>" + inline(head[k]) + "</th>";
- t += "</tr></thead><tbody>";
- for (i += 2; i < n && lines[i].indexOf("|") >= 0 && !/^\s*$/.test(lines[i]); i++) {
- var row = cells(lines[i]);
- t += "<tr>";
- for (k = 0; k < row.length; k++)
- t += "<td>" + inline(row[k]) + "</td>";
- t += "</tr>";
- }
- out += t + "</tbody></table>";
- continue;
- }
- if (/^\s*([-*+]|\d+[.)])\s+/.test(line)) {
- var ordered = /^\s*\d/.test(line), tag = ordered ? "ol" : "ul";
- out += "<" + tag + ">";
- for (; i < n && (m = /^\s*([-*+]|\d+[.)])\s+(.*)$/.exec(lines[i])); i++) {
- if ((/\d/.test(m[1])) !== ordered) break;
- out += "<li>" + inline(m[2]) + "</li>";
- }
- out += "</" + tag + ">";
- continue;
- }
- /* Always consume the current line so i advances even when it
- * matched none of the block branches above. */
- var para = lines[i++];
- for (; i < n && !/^\s*$/.test(lines[i]) &&
- !/^\s*(#{1,6}\s|>|`{3,}|~{3,}|([-*+]|\d+[.)])\s)/.test(lines[i]); i++)
- para += "\n" + lines[i];
- out += "<p>" + inline(para).replace(/\n/g, "<br/>") + "</p>";
- }
- return out;
-}
-
-document.addEventListener("DOMContentLoaded", function () {
- var nodes = document.querySelectorAll("div#cgit [data-markdown]"), i, el, text;
- for (i = 0; i < nodes.length; i++) {
- el = nodes[i];
- text = el.textContent;
- if (!text || text.length > MAX_BYTES)
- continue;
- try {
- el.innerHTML = render(text);
- /* The attribute doubles as the style hook for the
- * unrendered source, so drop it once rendered. */
- el.removeAttribute("data-markdown");
- } catch (e) {
- /* leave the escaped source in place on any failure */
- }
- }
-}, false);
-
-})();
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index 049b234..32f8b86 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -32,8 +32,9 @@ about-filter::
get the content of the about-file on its STDIN, the name of the file
as the first argument, and the STDOUT from the command will be
included verbatim on the about page. Default value: none. When no
- about-filter is set, a markdown readme is instead rendered by the
- bundled cgit.js (see enable-markdown). See also: "FILTER API".
+ about-filter is set, the readme is escaped and served as plain text.
+ A bundled Lua filter, about-render.lua, renders markdown, man pages
+ and plain text when about-filter points at it. See also: "FILTER API".
agefile::
Specifies a path, relative to each repository path, which can be used
@@ -241,14 +242,6 @@ enable-tree-group-dirs::
in git's own order, with directories and files intermixed by name.
Default value: "0".
-enable-markdown::
- Flag which, when set to "1", renders a markdown readme on the about
- page. cgit escapes the source and a small renderer bundled in cgit.js
- formats it in the browser, so the page stays readable as plain text
- when scripting is off. It applies only when no about-filter handles the
- file, and only to names ending in .md, .markdown, .mkd or .mdown.
- Default value: "1".
-
favicon::
Url used as link to the icon for cgit. Any path works, but keeping
the value "/favicon.ico" is still worthwhile, since clients that do
@@ -955,9 +948,9 @@ mimetype.svg=image/svg+xml
# extensions/syntax-highlight.lua highlights through the Scintillua lexers.
# source-filter=lua:/usr/share/cgit/extensions/syntax-highlight.lua
-# Markdown about pages render client-side by default (see enable-markdown).
-# For other formats such as manpages, point about-filter at your own script.
-# about-filter=/var/www/cgit/filters/my-about-formatter
+# About pages are escaped plain text unless an about-filter is set. The shipped
+# extensions/about-render.lua renders markdown, man pages and plain text.
+# about-filter=lua:/usr/share/cgit/extensions/about-render.lua
##
## Search for these files in the root of the default branch of repositories
diff --git a/examples/cgitrc b/examples/cgitrc
index 5626b58..bb87a9b 100644
--- a/examples/cgitrc
+++ b/examples/cgitrc
@@ -11,9 +11,9 @@
# directory, or list repositories by hand in the per-repository section at
# the end of this file.
#
-# In this fork, markdown readmes are rendered in the browser, so no
-# about-filter is needed for them. Source syntax highlighting is optional
-# and ships as a source-filter, see the filter section below.
+# About pages (markdown, man and plain-text readmes) render through the
+# bundled about-render.lua about-filter, see the filter section below. Source
+# syntax highlighting is optional and ships as a source-filter there too.
#
# One key=value pair per line. Lines starting with # are comments.
@@ -237,10 +237,6 @@ enable-tree-linenumbers=1
# Default is 0.
enable-tree-group-dirs=0
-# Render a markdown readme client-side on the about page. Values are 0 or 1.
-# Default is 1.
-enable-markdown=1
-
# Default maximum statistics period. Leaving this unset disables statistics.
# Values are week, month, quarter or year. Default is unset.
#max-stats=week
@@ -288,10 +284,10 @@ enable-http-clone=1
# 0 or 1. Default is 0.
enable-filter-overrides=0
-# Filter command used to format about-page content. Markdown already renders
-# in the browser, so this is only for other formats such as man pages. Value
-# is a command optionally prefixed with exec or lua. Default is none.
-#about-filter=exec:/path/to/your-command
+# Filter command used to format about-page content. The bundled about-render.lua
+# renders markdown, man pages and plain text. Value is a command optionally
+# prefixed with exec or lua. Default is none.
+#about-filter=lua:/usr/share/cgit/extensions/about-render.lua
# Filter command used to format commit messages, for example to turn object
# names and issue numbers into links. Value is a command optionally prefixed
@@ -563,4 +559,3 @@ noplainemail=0
# enable-filter-overrides is 1. Value is a command. Default is the global
# email-filter value.
#repo.email-filter=lua:/usr/share/cgit/extensions/email-gravatar.lua
-
diff --git a/extensions/about-render.lua b/extensions/about-render.lua
new file mode 100644
index 0000000..073b531
--- /dev/null
+++ b/extensions/about-render.lua
This diff is too large to be rendered inline. View it on its own page.
diff --git a/source/cgit.c b/source/cgit.c
index 5af2232..74600a7 100644
--- a/source/cgit.c
+++ b/source/cgit.c
@@ -211,8 +211,6 @@ static void config_cb(const char *name, const char *value)
ctx.cfg.enable_tree_linenumbers = atoi(value);
else if (!strcmp(name, "enable-tree-group-dirs"))
ctx.cfg.enable_tree_group_dirs = atoi(value);
- else if (!strcmp(name, "enable-markdown"))
- ctx.cfg.enable_markdown = atoi(value);
else if (!strcmp(name, "enable-git-config"))
ctx.cfg.enable_git_config = atoi(value);
else if (!strcmp(name, "enable-cache-list"))
@@ -420,7 +418,6 @@ static void prepare_context(void)
ctx.cfg.enable_http_clone = 1;
ctx.cfg.enable_index_owner = 1;
ctx.cfg.enable_tree_linenumbers = 1;
- ctx.cfg.enable_markdown = 1;
ctx.cfg.enable_git_config = 0;
ctx.cfg.max_repo_count = 50;
ctx.cfg.max_commit_count = 50;
diff --git a/source/cgit.h b/source/cgit.h
index b7caaf5..966851c 100644
--- a/source/cgit.h
+++ b/source/cgit.h
@@ -243,7 +243,6 @@ struct cgit_config {
int enable_html_serving;
int enable_tree_linenumbers;
int enable_tree_group_dirs;
- int enable_markdown;
int enable_git_config;
int enable_cache_list;
int local_time;
diff --git a/source/ui-summary.c b/source/ui-summary.c
index 8dd191b..80d1e5b 100644
--- a/source/ui-summary.c
+++ b/source/ui-summary.c
@@ -105,17 +105,6 @@ static char* append_readme_path(const char *filename, const char *ref, const cha
return full_path;
}
-static int readme_is_markdown(const char *filename)
-{
- const char *ext = strrchr(filename, '.');
-
- if (!ext || !ext[1])
- return 0;
- ext++;
- return !strcasecmp(ext, "md") || !strcasecmp(ext, "markdown") ||
- !strcasecmp(ext, "mkd") || !strcasecmp(ext, "mdown");
-}
-
void cgit_print_repo_readme(const char *path)
{
char *filename, *ref, *mimetype;
@@ -146,29 +135,14 @@ void cgit_print_repo_readme(const char *path)
}
html("<div id='summary'>");
- if (!ctx.repo->about_filter && ctx.cfg.enable_markdown &&
- readme_is_markdown(filename)) {
- /* No about-filter is set, so hand the markdown source to the
- * built-in client-side renderer in cgit.js. The source is
- * escaped here and rendered in the browser, and it degrades to
- * readable plain text when scripting is off.
- */
- html("<div class='markdown' data-markdown>");
- if (ref) {
- cgit_print_file(filename, ref, 1, 1);
- } else {
- struct strbuf sb = STRBUF_INIT;
- if (strbuf_read_file(&sb, filename, 0) >= 0)
- html_txt(sb.buf);
- strbuf_release(&sb);
- }
- html("</div>");
- } else if (!ctx.repo->about_filter) {
+ if (!ctx.repo->about_filter) {
/* No about-filter is configured, so there is nothing to turn
* the readme source into safe HTML. Escape it rather than serve
* repo content raw, which would let an untrusted repository
* inject script into the about page. The pre keeps the line
- * structure of the text, which bare escaped output loses.
+ * structure of the text, which bare escaped output loses. Point
+ * about-filter at the bundled about-render.lua to render a
+ * markdown or man readme instead, see cgitrc.5.txt.
*/
html("<pre class='plaintext'>");
if (ref) {
diff --git a/tests/t0200-security.sh b/tests/t0200-security.sh
index 83cdbfd..425708e 100644
--- a/tests/t0200-security.sh
+++ b/tests/t0200-security.sh
@@ -73,7 +73,7 @@ test_expect_success 'a small blob is still served' '
'
# --- Readme rendering escapes untrusted repository content ------------------
-test_expect_success 'markdown readme is escaped and marked for the client' '
+test_expect_success 'markdown readme without a filter is escaped as plain text' '
{
echo "virtual-root=/" &&
echo "cache-size=0" &&
@@ -82,7 +82,7 @@ test_expect_success 'markdown readme is escaped and marked for the client' '
echo "repo.readme=master:README.md"
} >secmdrc &&
CGIT_CONFIG="$PWD/secmdrc" QUERY_STRING="url=md/about/" cgit >tmp &&
- grep "data-markdown" tmp &&
+ grep "pre class=.plaintext." tmp &&
grep "&lt;script&gt;" tmp &&
! grep "<script>alert(1)</script>" tmp
'