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=<ref>" -> the log of that branch / tag
+ * "/log?id=<commit hash>" -> that commit
+ */
+function sai_weburl_link(e, suffix, text) {
+ if (!e || !e.weburl)
+ return san(text);
+
+ return "<a href=\"" + san(e.weburl + (suffix || "")) + "\">" +
+ san(text) + "</a>";
+}
+
function get_appropriate_ws_url()
{
var pcol;
@@ -1473,29 +1489,35 @@ function sai_event_summary_render(o, now_ut, reset_all_icon)
s +=
"<td><table class=\"nomar\">" +
"<tr><td class=\"nomar\" colspan=2>" +
- "<span class=\"e1\">" + san(e.repo_name);
+ "<span class=\"e1\">" + sai_weburl_link(e, "", e.repo_name);
if (e.sec)
s += " <img class=\"bico\" src=\"/sai/locked.svg\">";
s += "</span></td></tr><tr><td class=\"nomar\" colspan=2><span class=\"e2\">";
if (e.ref.substr(0, 11) === "refs/heads/") {
s += "<img class=\"branch\">" +
- 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 += "<img class=\"tag\">" +
- 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 += "</span></td></tr><tr><td class=\"nomar e6\">" +
- san(sai_event_hash_display(e.hash)) +
+ sai_weburl_link(e, "/log?id=" + encodeURIComponent(e.hash),
+ sai_event_hash_display(e.hash)) +
"</td><td class=\"e6 nomar\">" +
agify(now_ut, e.created) + "</td></tr>";
s += "</table>" +
"</td>";
} else {
- s +="<td><table><tr><td class=\"e6 nomar\">" + san(sai_event_hash_display(e.hash)) + " " + agify(now_ut, e.created) +
+ s +="<td><table><tr><td class=\"e6 nomar\">" +
+ sai_weburl_link(e, "/log?id=" + encodeURIComponent(e.hash),
+ sai_event_hash_display(e.hash)) + " " +
+ agify(now_ut, e.created) +
"</td></tr><tr><td class=\"nomar e6\" id=\"sumbs-" + e.uuid + "\"></td></tr>" +
"</table></td>";
}
@@ -1713,7 +1735,9 @@ function render_sb_events()
s += "<span class=\"sb-event-when\">" + san(sai_sb_fmt_when(e.created)) +
" <span class='age-0' ut='" + e.created + "'>" +
agify(now_ut, e.created) + "</span></span>";
- s += "<span class=\"sb-event-tag\">" + san(sai_event_hash_display(e.hash)) + "</span>";
+ s += "<span class=\"sb-event-tag\">" +
+ sai_weburl_link(e, "/log?id=" + encodeURIComponent(e.hash),
+ sai_event_hash_display(e.hash)) + "</span>";
s += "<span class=\"sb-event-status\" id=\"sbsum-" + san(e.uuid) + "\"></span>";
/* progress bar slot (inline, takes remaining width), filled by sai_sb_render_event_summary() */
s += "<span class=\"sb-event-bar\" id=\"sbbar-" + san(e.uuid) + "\"></span>";
@@ -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 += "<span class=\"event-tasks-title\">" + san(e.repo_name) + " (" + san(refName) + ") - " + san(sai_event_hash_display(e.hash)) + "</span>";
+ s += "<span class=\"event-tasks-title\">" +
+ 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)) +
+ "</span>";
/* admin-only restart-all / delete-event controls live here now */
if (!gitohashi_integ && auth_state === SaiAuthState.LOGGED_IN_GRANT_ADMIN) {
s += "<img class=\"rebuild\" alt=\"rebuild all\" src=\"/sai/rebuild.png\" " +
diff --git a/src/common/include/private.h b/src/common/include/private.h
index 4edefc3..ccec1a6 100644
--- a/src/common/include/private.h
+++ b/src/common/include/private.h
@@ -403,6 +403,8 @@ typedef struct sai_event {
struct lws_dll2 list;
char repo_name[65];
char repo_fetchurl[96];
+ /* optional http(s) url for browsing the repo in a web ui */
+ char repo_weburl[128];
char ref[65];
char hash[65];
char uuid[65];
diff --git a/src/common/struct-metadata.c b/src/common/struct-metadata.c
index f4c840d..6746e45 100644
--- a/src/common/struct-metadata.c
+++ b/src/common/struct-metadata.c
@@ -132,6 +132,7 @@ const lws_struct_map_t lsm_event[] = {
LSM_UNSIGNED (sai_event_t, uid, "uid"),
LSM_CARRAY (sai_event_t, repo_name, "repo_name"),
LSM_CARRAY (sai_event_t, repo_fetchurl, "repo_fetchurl"),
+ LSM_CARRAY (sai_event_t, repo_weburl, "weburl"),
LSM_CARRAY (sai_event_t, ref, "ref"),
LSM_CARRAY (sai_event_t, hash, "hash"),
LSM_CARRAY (sai_event_t, uuid, "uuid"),
diff --git a/src/server/s-comms.c b/src/server/s-comms.c
index d3bc117..6092c05 100644
--- a/src/server/s-comms.c
+++ b/src/server/s-comms.c
@@ -215,6 +215,21 @@ s_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->server.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,