mirror of
https://git.ethanthesleepy.one/ethanaobrien/ew
synced 2026-08-26 23:12:20 +08:00
Show public custom song titles on the clear rate page
This commit is contained in:
@@ -239,6 +239,24 @@ pub fn export_allowed(music_id: i64, viewer: Option<i64>) -> Result<(), &'static
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The display title for a PUBLIC custom song, for the clear-rate page.
|
||||||
|
// Private/shared songs return None - their names (and existence) must not
|
||||||
|
// leak beyond the visibility rules, so this never falls back past the
|
||||||
|
// explicit visibility check (an absent row is None, not "public")
|
||||||
|
pub fn public_song_title(music_id: i64, english: bool) -> Option<String> {
|
||||||
|
if !(FIRST_MUSIC_ID..=LAST_MUSIC_ID).contains(&music_id) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let visibility = DATABASE.lock_and_select("SELECT visibility FROM songs WHERE music_id=?1", params!(music_id)).ok()?;
|
||||||
|
if visibility != "public" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let song = get_song(music_id)?;
|
||||||
|
let name = song["name"].as_str().unwrap_or("").to_string();
|
||||||
|
let name_en = song["name_en"].as_str().unwrap_or("").to_string();
|
||||||
|
Some(if english && !name_en.is_empty() { name_en } else { name })
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_music_ids_for_user(user_id: i64) -> JsonValue {
|
pub fn get_music_ids_for_user(user_id: i64) -> JsonValue {
|
||||||
DATABASE.lock_and_select_all("
|
DATABASE.lock_and_select_all("
|
||||||
SELECT music_id FROM songs
|
SELECT music_id FROM songs
|
||||||
|
|||||||
@@ -173,9 +173,22 @@ fn get_song_title(live_id: i32, english: bool) -> String {
|
|||||||
if !details.is_null() {
|
if !details.is_null() {
|
||||||
return details["name"].to_string();
|
return details["name"].to_string();
|
||||||
}
|
}
|
||||||
|
// Custom songs aren't in the official music mst (their live_id ==
|
||||||
|
// music_id). PUBLIC ones show their real title; private/shared ones are
|
||||||
|
// already filtered out of the page and would stay "Unknown Song" even if
|
||||||
|
// one slipped through - the lookup only ever answers for public songs
|
||||||
|
if let Some(title) = crate::router::custom_song::public_song_title(live_id as i64, english) {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
String::from("Unknown Song")
|
String::from("Unknown Song")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Titles land inside HTML text and attributes; custom song names are user
|
||||||
|
// input, so escape them (official names contain nothing that needs it)
|
||||||
|
fn html_escape(text: &str) -> String {
|
||||||
|
text.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_pass_percent(failed: i64, pass: i64) -> String {
|
fn get_pass_percent(failed: i64, pass: i64) -> String {
|
||||||
let total = (failed + pass) as f64;
|
let total = (failed + pass) as f64;
|
||||||
if failed + pass == 0 {
|
if failed + pass == 0 {
|
||||||
@@ -315,8 +328,8 @@ fn get_html() -> JsonValue {
|
|||||||
if total == 0 { 0.0 } else { pass as f64 / total as f64 }
|
if total == 0 { 0.0 } else { pass as f64 / total as f64 }
|
||||||
};
|
};
|
||||||
|
|
||||||
let title_jp = get_song_title(info.live_id, false);
|
let title_jp = html_escape(&get_song_title(info.live_id, false));
|
||||||
let title_en = get_song_title(info.live_id, true);
|
let title_en = html_escape(&get_song_title(info.live_id, true));
|
||||||
|
|
||||||
let normal_txt = get_pass_percent(info.normal_failed, info.normal_pass);
|
let normal_txt = get_pass_percent(info.normal_failed, info.normal_pass);
|
||||||
let hard_txt = get_pass_percent(info.hard_failed, info.hard_pass);
|
let hard_txt = get_pass_percent(info.hard_failed, info.hard_pass);
|
||||||
|
|||||||
@@ -124,6 +124,15 @@ pub fn hidden_live_ids() -> JsonValue {
|
|||||||
database::non_public_music_ids()
|
database::non_public_music_ids()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The clear-rate page shows real titles for PUBLIC custom songs; anything
|
||||||
|
// else stays exactly as hidden as before
|
||||||
|
pub fn public_song_title(music_id: i64, english: bool) -> Option<String> {
|
||||||
|
if disabled() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
database::public_song_title(music_id, english)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn hidden_live_ids_for_user(uid: i64) -> JsonValue {
|
pub fn hidden_live_ids_for_user(uid: i64) -> JsonValue {
|
||||||
if disabled() {
|
if disabled() {
|
||||||
return array![];
|
return array![];
|
||||||
@@ -1579,6 +1588,51 @@ mod tests {
|
|||||||
assert_eq!(call(&new_md5, "json").status(), actix_web::http::StatusCode::OK);
|
assert_eq!(call(&new_md5, "json").status(), actix_web::http::StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The public clear-rate HTML page shows the REAL title for public custom
|
||||||
|
// songs (escaped - names are user input), keeps private songs entirely
|
||||||
|
// absent, and prefers name_en for the EN title attribute
|
||||||
|
#[test]
|
||||||
|
fn clearrate_html_shows_public_custom_song_titles() {
|
||||||
|
use actix_web::test::TestRequest;
|
||||||
|
use crate::router::clear_rate;
|
||||||
|
let _lock = crate::runtime::lock_test_data_path();
|
||||||
|
|
||||||
|
let public_id = database::next_music_id();
|
||||||
|
database::insert_song(public_id, 6100, &object!{
|
||||||
|
music_id: public_id,
|
||||||
|
name: "Public <Song> & \"Co\"",
|
||||||
|
name_en: "Public Song EN"
|
||||||
|
}, "public", &array![], false);
|
||||||
|
let private_id = database::next_music_id();
|
||||||
|
database::insert_song(private_id, 6100, &object!{
|
||||||
|
music_id: private_id,
|
||||||
|
name: "Top Secret Anthem"
|
||||||
|
}, "private", &array![], false);
|
||||||
|
for id in [public_id, private_id] {
|
||||||
|
clear_rate::live_completed(id, 1, false, 100, 6100);
|
||||||
|
}
|
||||||
|
clear_rate::invalidate_cache();
|
||||||
|
|
||||||
|
let html = actix_web::rt::System::new().block_on(async {
|
||||||
|
let resp = clear_rate::clearrate_html(TestRequest::default().to_http_request()).await;
|
||||||
|
let body = actix_web::body::to_bytes(resp.into_body()).await.unwrap();
|
||||||
|
String::from_utf8_lossy(&body).to_string()
|
||||||
|
});
|
||||||
|
// The public song's real (escaped) title, JP cell and EN attribute
|
||||||
|
assert!(html.contains("Public <Song> & "Co""), "public title missing");
|
||||||
|
assert!(html.contains("Public Song EN"), "EN title missing");
|
||||||
|
// Never the raw unescaped markup
|
||||||
|
assert!(!html.contains("Public <Song>"), "title not escaped");
|
||||||
|
// The private song leaks neither name nor row
|
||||||
|
assert!(!html.contains("Top Secret Anthem"), "private name leaked");
|
||||||
|
|
||||||
|
// The title lookup itself only answers for public songs
|
||||||
|
assert_eq!(database::public_song_title(public_id, false), Some(String::from("Public <Song> & \"Co\"")));
|
||||||
|
assert_eq!(database::public_song_title(public_id, true), Some(String::from("Public Song EN")));
|
||||||
|
assert_eq!(database::public_song_title(private_id, false), None);
|
||||||
|
assert_eq!(database::public_song_title(private_id + 5000, false), None);
|
||||||
|
}
|
||||||
|
|
||||||
// The JSON clear-rate endpoint filters non-public custom songs per requesting
|
// The JSON clear-rate endpoint filters non-public custom songs per requesting
|
||||||
// user: the owner sees all of theirs, a shared user sees the songs shared with
|
// user: the owner sees all of theirs, a shared user sees the songs shared with
|
||||||
// them, everyone else sees only public ones. Official (stock) live ids are
|
// them, everyone else sees only public ones. Official (stock) live ids are
|
||||||
|
|||||||
Reference in New Issue
Block a user