From 84f5f24b6ab711f74626b6578a93cf3862bf2760 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 02:10:56 +0000 Subject: [PATCH] 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. --- src/auth.rs | 13 ++---------- src/html.rs | 10 +++++++++ src/main.rs | 28 +++++++++--------------- src/web.rs | 61 +++++++++++++++++++++++------------------------------ 4 files changed, 48 insertions(+), 64 deletions(-) create mode 100644 src/html.rs diff --git a/src/auth.rs b/src/auth.rs index bdafeb0..78399b1 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -10,7 +10,7 @@ use rand::{ use serde::Deserialize; use serde_json::Value; -use crate::{AppState, db, intervals::IntervalsClient}; +use crate::{AppState, db, html, intervals::IntervalsClient}; #[derive(Debug, Deserialize)] pub struct OAuthCallback { @@ -209,7 +209,7 @@ button{{background:#2563eb;color:white;border:0;border-radius:8px;padding:.7rem "#, - escape_html(message) + html::escape(message) ) } @@ -315,12 +315,3 @@ pub async fn logout(State(state): State, jar: CookieJar) -> impl IntoR (jar, Redirect::to("/")).into_response() } - -fn escape_html(value: &str) -> String { - value - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") -} diff --git a/src/html.rs b/src/html.rs new file mode 100644 index 0000000..5d5c073 --- /dev/null +++ b/src/html.rs @@ -0,0 +1,10 @@ +/// Escapes text for safe inclusion in HTML (element content and +/// double-quoted attribute values). +pub fn escape(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} diff --git a/src/main.rs b/src/main.rs index a1b02fa..3ff3c8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod auth; mod config; mod db; mod geo; +mod html; mod intervals; mod leaderboard; mod model; @@ -225,8 +226,8 @@ a {{

"#, - web_escape(&session.display_name), - web_escape(&session.intervals_athlete_id), + html::escape(&session.display_name), + html::escape(&session.intervals_athlete_id), url_segment(&session.intervals_athlete_id), ); @@ -388,7 +389,7 @@ input[type="datetime-local"] { html.push_str(&format!( "

Sync: {}

", - web_escape(&session.display_name) + html::escape(&session.display_name) )); html.push_str( @@ -465,8 +466,8 @@ input[type="datetime-local"] { "#, url_segment(&athlete_id), - web_escape(&oldest_input), - web_escape(&newest_input), + html::escape(&oldest_input), + html::escape(&newest_input), )); } @@ -494,9 +495,9 @@ input[type="datetime-local"] { {} · {} · {} "#, - web_escape(&activity.name), - web_escape(&activity.activity_type), - web_escape(activity.start_time.as_deref().unwrap_or("")), + html::escape(&activity.name), + html::escape(&activity.activity_type), + html::escape(activity.start_time.as_deref().unwrap_or("")), distance, )); @@ -557,7 +558,7 @@ Aktivität {} wurde importiert. "#, url_segment(&athlete_id), - web_escape(&activity_id), + html::escape(&activity_id), url_segment(&athlete_id), ))) } @@ -728,15 +729,6 @@ fn http_error(error: anyhow::Error) -> (axum::http::StatusCode, String) { ) } -fn web_escape(value: &str) -> String { - value - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") -} - fn url_segment(value: &str) -> String { urlencoding::encode(value).into_owned() } diff --git a/src/web.rs b/src/web.rs index 4910b96..c4fae09 100644 --- a/src/web.rs +++ b/src/web.rs @@ -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#"
Angemeldet als {}
Gruppen Logout
"#, - escape_html(name) + html::escape(name) )); } else { html.push_str( @@ -410,7 +410,7 @@ fn render_index( "", id, selected, - escape_html(name) + html::escape(name) )); } @@ -427,8 +427,8 @@ fn render_index( html.push_str("
"); html.push_str(&format!( "

{} → {}

", - escape_html(&group.from_county), - escape_html(&group.to_county) + html::escape(&group.from_county), + html::escape(&group.to_county) )); html.push_str(&format!( "
10-Minuten-Fenster · {} · Standort ±10 m
", @@ -454,7 +454,7 @@ fn render_index( .map(|url| { format!( "Intervall", - escape_html(url) + html::escape(url) ) }) .unwrap_or_else(|| "".into()); @@ -478,11 +478,11 @@ fn render_index(
"#, 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(

{}

Athlet: {} · Aktivität: {}
"#, - 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#"
Start: {}
"#, - 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#"
"#, @@ -573,7 +573,7 @@ fn render_activity( .map(|url| { format!( "Intervall", - escape_html(url) + html::escape(url) ) }) .unwrap_or_else(|| "—".into()); @@ -592,10 +592,10 @@ fn render_activity( {} → {}{} {}{}{}"#, 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#"
← Leaderboard

Leaderboard-Gruppen

Gruppen werden für {} verwaltet.
"#, - escape_html(owner_name) + html::escape(owner_name) )); html.push_str( @@ -639,7 +639,7 @@ fn render_groups( html.push_str(&format!( r#""#, id, - escape_html(name) + html::escape(name) )); } html.push_str(r#"
"#); @@ -651,7 +651,7 @@ fn render_groups(
"#, group_id, - escape_html(name) + html::escape(name) )); for (id, athlete_name) in athletes { @@ -660,7 +660,7 @@ fn render_groups( r#""#, id, checked, - escape_html(athlete_name) + html::escape(athlete_name) )); } @@ -746,12 +746,3 @@ if(activityMapEl){ "#; - -fn escape_html(value: &str) -> String { - value - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - .replace('"', """) - .replace('\'', "'") -}