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:
parent
1a0ee71e6b
commit
84f5f24b6a
4 changed files with 48 additions and 64 deletions
13
src/auth.rs
13
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
|
|||
</div>
|
||||
</body>
|
||||
</html>"#,
|
||||
escape_html(message)
|
||||
html::escape(message)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -315,12 +315,3 @@ pub async fn logout(State(state): State<AppState>, jar: CookieJar) -> impl IntoR
|
|||
|
||||
(jar, Redirect::to("/")).into_response()
|
||||
}
|
||||
|
||||
fn escape_html(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
|
|
|||
10
src/html.rs
Normal file
10
src/html.rs
Normal file
|
|
@ -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('\'', "'")
|
||||
}
|
||||
28
src/main.rs
28
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 {{
|
|||
</p>
|
||||
</body>
|
||||
</html>"#,
|
||||
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!(
|
||||
"<h1>Sync: {}</h1>",
|
||||
web_escape(&session.display_name)
|
||||
html::escape(&session.display_name)
|
||||
));
|
||||
|
||||
html.push_str(
|
||||
|
|
@ -465,8 +466,8 @@ input[type="datetime-local"] {
|
|||
</form>
|
||||
</div>"#,
|
||||
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"] {
|
|||
{} · {} · {}
|
||||
</div>
|
||||
</div>"#,
|
||||
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.
|
|||
</body>
|
||||
</html>"#,
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
61
src/web.rs
61
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#"<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('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue