dedupe HTML escaping into a shared html module

web_escape (main.rs) and escape_html (auth.rs, web.rs) were three
byte-identical copies of the same & < > " ' replacement chain. Moved
into src/html.rs as html::escape and used from all three call sites.
This commit is contained in:
Claude 2026-08-13 02:10:56 +00:00 committed by Jonas Rabenstein
commit 84f5f24b6a
4 changed files with 48 additions and 64 deletions

View file

@ -13,7 +13,7 @@ use serde_json::Value;
use sqlx::Row;
use crate::{
AppState, db, leaderboard,
AppState, db, html, leaderboard,
model::{ActivityCrossing, LeaderboardGroup},
};
@ -379,7 +379,7 @@ fn render_index(
r#"<div>Angemeldet als <strong>{}</strong><br>
<a class="button secondary" href="/groups">Gruppen</a>
<a class="button" href="/logout">Logout</a></div>"#,
escape_html(name)
html::escape(name)
));
} else {
html.push_str(
@ -410,7 +410,7 @@ fn render_index(
"<option value=\"{}\"{}>{}</option>",
id,
selected,
escape_html(name)
html::escape(name)
));
}
@ -427,8 +427,8 @@ fn render_index(
html.push_str("<section class=\"card\">");
html.push_str(&format!(
"<h2>{} → {}</h2>",
escape_html(&group.from_county),
escape_html(&group.to_county)
html::escape(&group.from_county),
html::escape(&group.to_county)
));
html.push_str(&format!(
"<div class=\"small\">10-Minuten-Fenster · {} · Standort ±10 m</div>",
@ -454,7 +454,7 @@ fn render_index(
.map(|url| {
format!(
"<a href=\"{}\" target=\"_blank\" rel=\"noreferrer\">Intervall</a>",
escape_html(url)
html::escape(url)
)
})
.unwrap_or_else(|| "<span class=\"small\">—</span>".into());
@ -478,11 +478,11 @@ fn render_index(
<td><div class="mini-map" data-lat="{}" data-lon="{}"></div></td>
</tr>"#,
row.rank,
escape_html(&row.athlete),
escape_html(&row.crossing_time.to_rfc3339()),
html::escape(&row.athlete),
html::escape(&row.crossing_time.to_rfc3339()),
row.crossing_time.format("%H:%M:%S%.3f UTC"),
escape_html(&row.activity_url),
escape_html(&row.activity_id),
html::escape(&row.activity_url),
html::escape(&row.activity_id),
interval,
row.lat,
row.lon,
@ -522,18 +522,18 @@ fn render_activity(
<section class="card">
<h2>{}</h2>
<div class="small">Athlet: {} · Aktivität: {}</div>"#,
escape_html(title),
escape_html(&athlete_name),
escape_html(sport),
escape_html(title),
escape_html(&athlete_name),
escape_html(activity_id),
html::escape(title),
html::escape(&athlete_name),
html::escape(sport),
html::escape(title),
html::escape(&athlete_name),
html::escape(activity_id),
));
if let Some(start) = start_time {
html.push_str(&format!(
r#"<div class="small activity-start" data-time="{}">Start: {}</div>"#,
escape_html(&start.to_rfc3339()),
html::escape(&start.to_rfc3339()),
start.format("%Y-%m-%d %H:%M:%S UTC")
));
}
@ -552,7 +552,7 @@ fn render_activity(
})
})
.collect();
let points_json = escape_html(&serde_json::to_string(&points).unwrap_or_else(|_| "[]".into()));
let points_json = html::escape(&serde_json::to_string(&points).unwrap_or_else(|_| "[]".into()));
html.push_str(&format!(
r#"<div id="activity-map" class="activity-map" data-points="{}"></div>"#,
@ -573,7 +573,7 @@ fn render_activity(
.map(|url| {
format!(
"<a href=\"{}\" target=\"_blank\" rel=\"noreferrer\">Intervall</a>",
escape_html(url)
html::escape(url)
)
})
.unwrap_or_else(|| "".into());
@ -592,10 +592,10 @@ fn render_activity(
<td>{} {}</td><td class="rank">{}</td>
<td>{}</td><td>{}</td><td>{}</td></tr>"#,
index + 1,
escape_html(&crossing.crossing_time.to_rfc3339()),
html::escape(&crossing.crossing_time.to_rfc3339()),
crossing.crossing_time.format("%H:%M:%S%.3f UTC"),
escape_html(&crossing.from_county),
escape_html(&crossing.to_county),
html::escape(&crossing.from_county),
html::escape(&crossing.to_county),
crossing.rank,
power,
hr,
@ -626,7 +626,7 @@ fn render_groups(
html.push_str(&format!(
r#"<header><div><a href="/">← Leaderboard</a><h1>Leaderboard-Gruppen</h1>
<div class="small">Gruppen werden für {} verwaltet.</div></div></header>"#,
escape_html(owner_name)
html::escape(owner_name)
));
html.push_str(
@ -639,7 +639,7 @@ fn render_groups(
html.push_str(&format!(
r#"<label><input type="checkbox" name="members" value="{}"> {}</label>"#,
id,
escape_html(name)
html::escape(name)
));
}
html.push_str(r#"</div><button type="submit">Gruppe anlegen</button></form></section>"#);
@ -651,7 +651,7 @@ fn render_groups(
<input type="text" name="name" value="{}" required>
<div class="members">"#,
group_id,
escape_html(name)
html::escape(name)
));
for (id, athlete_name) in athletes {
@ -660,7 +660,7 @@ fn render_groups(
r#"<label><input type="checkbox" name="members" value="{}"{}> {}</label>"#,
id,
checked,
escape_html(athlete_name)
html::escape(athlete_name)
));
}
@ -746,12 +746,3 @@ if(activityMapEl){
</script>
</body></html>
"#;
fn escape_html(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&#39;")
}