Author: Andy Green Date: Sun Aug 23 18:58:15 2026 +0100 js-weburl diff --git a/assets/sai.css b/assets/sai.css index f274d74..6f498c6 100644 --- a/assets/sai.css +++ b/assets/sai.css @@ -2073,6 +2073,27 @@ div.ibuil.vm-builder { color: #ffffff; } +/* + * Links into the repo's gitweb (event weburl): keep the surrounding text + * styling (so pass/fail backgrounds still read), hint with hover underline + */ +.event-tasks-title a, +.sb-event-tag a, +span.e1 a, +span.e2 a, +td.e6 a { + color: inherit; + text-decoration: none; +} + +.event-tasks-title a:hover, +.sb-event-tag a:hover, +span.e1 a:hover, +span.e2 a:hover, +td.e6 a:hover { + text-decoration: underline; +} + .tasks-table-display { width: 100%; border-collapse: collapse; diff --git a/assets/sai.js b/assets/sai.js index 9ec4891..8dc8b4e 100644 --- a/assets/sai.js +++ b/assets/sai.js @@ -995,6 +995,22 @@ function sai_event_hash_display(hash) { return "sai-" + hash.substring(0, 8); } +/* + * If the event was notified with a repository weburl (the base URL of the + * repo's gitweb, eg gitohashi), render `text` as a link into it; otherwise + * return the plain sanitized text. suffix is appended to the weburl: + * "" -> the repo summary page + * "/log?h=" -> the log of that branch / tag + * "/log?id=" -> that commit + */ +function sai_weburl_link(e, suffix, text) { + if (!e || !e.weburl) + return san(text); + + return "" + + san(text) + ""; +} + function get_appropriate_ws_url() { var pcol; @@ -1473,29 +1489,35 @@ function sai_event_summary_render(o, now_ut, reset_all_icon) s += "" + ""; s += "
" + - "" + san(e.repo_name); + "" + sai_weburl_link(e, "", e.repo_name); if (e.sec) s += " "; s += "
"; if (e.ref.substr(0, 11) === "refs/heads/") { s += "" + - san(e.ref.substr(11)); + sai_weburl_link(e, "/log?h=" + encodeURIComponent(e.ref.substr(11)), + e.ref.substr(11)); } else if (e.ref.substr(0, 10) === "refs/tags/") { s += "" + - san(e.ref.substr(10)); + sai_weburl_link(e, "/log?h=" + encodeURIComponent(e.ref.substr(10)), + e.ref.substr(10)); } else s += san(e.ref); s += "
" + - san(sai_event_hash_display(e.hash)) + + sai_weburl_link(e, "/log?id=" + encodeURIComponent(e.hash), + sai_event_hash_display(e.hash)) + "" + agify(now_ut, e.created) + "
" + ""; } else { - s +=""; } @@ -1713,7 +1735,9 @@ function render_sb_events() s += "" + san(sai_sb_fmt_when(e.created)) + " " + agify(now_ut, e.created) + ""; - s += "" + san(sai_event_hash_display(e.hash)) + ""; + s += "" + + sai_weburl_link(e, "/log?id=" + encodeURIComponent(e.hash), + sai_event_hash_display(e.hash)) + ""; s += ""; /* progress bar slot (inline, takes remaining width), filled by sai_sb_render_event_summary() */ s += ""; @@ -1722,7 +1746,10 @@ function render_sb_events() } c.innerHTML = s; c.querySelectorAll(".sb-event-row").forEach(function(row) { - row.addEventListener("click", function() { + row.addEventListener("click", function(ev) { + /* embedded gitweb links navigate on their own */ + if (ev.target && ev.target.closest && ev.target.closest("a")) + return; selectEvent(row.getAttribute("data-uuid")); }); }); @@ -1859,7 +1886,13 @@ function render_selected_event_tasks(o) { if (e.state == 4 || e.state == 6) s += " comp_fail"; s += "\">"; var refName = e.ref.replace("refs/heads/", "").replace("refs/tags/", ""); - s += "" + san(e.repo_name) + " (" + san(refName) + ") - " + san(sai_event_hash_display(e.hash)) + ""; + s += "" + + sai_weburl_link(e, "", e.repo_name) + + " (" + sai_weburl_link(e, "/log?h=" + encodeURIComponent(refName), refName) + ")" + + " - " + + sai_weburl_link(e, "/log?id=" + encodeURIComponent(e.hash), + sai_event_hash_display(e.hash)) + + ""; /* admin-only restart-all / delete-event controls live here now */ if (!gitohashi_integ && auth_state === SaiAuthState.LOGGED_IN_GRANT_ADMIN) { s += "\"rebuildserver.pdb, + "ALTER TABLE events ADD COLUMN weburl varchar;", + NULL, NULL, &err); + if (err) + sqlite3_free(err); + } + sai_sqlite3_statement(vhd->server.pdb, "CREATE UNIQUE INDEX IF NOT EXISTS idx_event_uuid ON events(uuid);", "create event index"); if (lws_struct_sq3_create_table(vhd->server.pdb, diff --git a/src/server/s-notification.c b/src/server/s-notification.c index 8807adc..1eac80a 100644 --- a/src/server/s-notification.c +++ b/src/server/s-notification.c @@ -36,6 +36,7 @@ static const char * const paths[] = { "action", "repository.name", "repository.fetchurl", + "repository.weburl", "ref", "hash", "nonce", @@ -49,6 +50,7 @@ enum enum_paths { LEJPN_ACTION, LEJPN_REPOSITORY_NAME, LEJPN_REPOSITORY_FETCHURL, + LEJPN_REPOSITORY_WEBURL, LEJPN_REF, LEJPN_HASH, LEJPN_NONCE, @@ -879,6 +881,27 @@ sai_notification_lejp_cb(struct lejp_ctx *ctx, char reason) sizeof(sn->e.repo_fetchurl)); break; + case LEJPN_REPOSITORY_WEBURL: + /* + * weburl is optional, and only used to build links to the + * repo's web ui (eg, gitohashi) in the browser. Since repo + * data is attacker-influenced and this becomes an href, only + * accept a clean http(s) URL... anything else is dropped and + * the event stored without a weburl rather than rejecting the + * whole notification over a UI nicety. + */ + sn->e.repo_weburl[0] = '\0'; + + if ((!strncasecmp(ctx->buf, "https://", 8) || + !strncasecmp(ctx->buf, "http://", 7)) && + !sai_str_has_shell_metachars(ctx->buf)) + lws_strncpy(sn->e.repo_weburl, ctx->buf, + sizeof(sn->e.repo_weburl)); + else + lwsl_notice("%s: ignoring weburl '%s'\n", + __func__, ctx->buf); + break; + case LEJPN_REF: lws_strncpy(sn->e.ref, ctx->buf, sizeof(sn->e.ref)); /* diff --git a/src/web/w-comms.c b/src/web/w-comms.c index 465de59..9fd99d2 100644 --- a/src/web/w-comms.c +++ b/src/web/w-comms.c @@ -195,6 +195,21 @@ w_callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, return -1; } + /* + * create_table() is "if not exists", so existing event tables + * lack later columns like weburl... try to add them (fails + * harmlessly if the column is already there) + */ + { + char *err = NULL; + + sqlite3_exec(vhd->pdb, + "ALTER TABLE events ADD COLUMN weburl varchar;", + NULL, NULL, &err); + if (err) + sqlite3_free(err); + } + sai_sqlite3_statement(vhd->pdb, "CREATE UNIQUE INDEX IF NOT EXISTS idx_event_uuid ON events(uuid);", "create event index"); sai_sqlite3_statement(vhd->pdb,
" + san(sai_event_hash_display(e.hash)) + " " + agify(now_ut, e.created) + + s +="" + "
" + + sai_weburl_link(e, "/log?id=" + encodeURIComponent(e.hash), + sai_event_hash_display(e.hash)) + " " + + agify(now_ut, e.created) + "