From 1500f3de5431dfae15c3afb58fbb62d3c43098cc Mon Sep 17 00:00:00 2001 From: Robert Czechowski <robert@code-intelligence.com> Date: Fri, 20 May 2022 20:41:03 +0200 Subject: [PATCH] Handle error case of unknown {contest|task} id gracefully instead of panicking --- src/core.rs | 27 ++++++++++++++------------- src/db_conn.base.rs | 33 ++++++++++++++++++--------------- src/db_conn.rs | 10 +++++----- src/db_conn_postgres.rs | 32 +++++++++++++++++--------------- src/db_conn_sqlite_new.rs | 32 +++++++++++++++++--------------- src/webfw_iron.rs | 5 ++++- templates/jwinf/index.hbs | 2 +- 7 files changed, 76 insertions(+), 65 deletions(-) diff --git a/src/core.rs b/src/core.rs index 60454e54..4d048b2a 100644 --- a/src/core.rs +++ b/src/core.rs @@ -59,6 +59,7 @@ pub enum MedalError { UnmatchedPasswords, NotFound, AccountIncomplete, + UnknownId, OauthError(String), } @@ -413,7 +414,7 @@ pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token return Err(MedalError::AccountIncomplete); } - let contest = conn.get_contest_by_id_complete(contest_id); + let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?; let grades = conn.get_contest_user_grades(&session_token, contest_id); let mut opt_part = conn.get_participation(session.id, contest_id); @@ -613,7 +614,7 @@ pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, sessi data.insert("taskname".to_string(), to_json(&tasknames)); data.insert("result".to_string(), to_json(&results)); - let c = conn.get_contest_by_id(contest_id); + let c = conn.get_contest_by_id(contest_id).ok_or(MedalError::UnknownId)?; let ci = ContestInfo { id: c.id.unwrap(), name: c.name.clone(), duration: c.duration, public: c.public }; data.insert("contest".to_string(), to_json(&ci)); @@ -627,7 +628,7 @@ pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_toke -> MedalResult<()> { // TODO: Is _or_new the right semantic? We need a CSRF token anyway … let session = conn.get_session_or_new(&session_token).unwrap(); - let contest = conn.get_contest_by_id(contest_id); + let contest = conn.get_contest_by_id(contest_id).ok_or(MedalError::UnknownId)?; // Check logged in or open contest if contest.duration != 0 @@ -761,7 +762,7 @@ pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token return Err(MedalError::CsrfCheckFailed); } - let (t, _, contest) = conn.get_task_by_id_complete(task_id); + let (t, _, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?; match conn.get_participation(session.id, contest.id.expect("Value from database")) { None => return Err(MedalError::AccessDenied), @@ -826,12 +827,12 @@ pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token } pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, autosaveinterval: u64) - -> Result<MedalValue, i32> { + -> MedalResult<Result<MedalValue, i32>> { let session = conn.get_session_or_new(&session_token).unwrap(); - let (t, tg, contest) = conn.get_task_by_id_complete(task_id); + let (t, tg, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?; let grade = conn.get_taskgroup_user_grade(&session_token, tg.id.unwrap()); // TODO: Unwrap? - let tasklist = conn.get_contest_by_id_complete(contest.id.unwrap()); // TODO: Unwrap? + let tasklist = conn.get_contest_by_id_complete(contest.id.unwrap()).ok_or(MedalError::UnknownId)?; // TODO: Unwrap? let mut prevtaskgroup: Option<Taskgroup> = None; let mut nexttaskgroup: Option<Taskgroup> = None; @@ -854,7 +855,7 @@ pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str } match conn.get_own_participation(&session_token, contest.id.expect("Value from database")) { - None => Err(contest.id.unwrap()), + None => Ok(Err(contest.id.unwrap())), Some(participation) => { let mut data = json_val::Map::new(); data.insert("subtasks".to_string(), to_json(&subtaskstars)); @@ -896,10 +897,10 @@ pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str let taskpath = format!("{}{}", contest.location, &tasklocation); data.insert("taskpath".to_string(), to_json(&taskpath)); - Ok((template, data)) + Ok(Ok((template, data))) } else { // Contest over - Err(contest.id.unwrap()) + Ok(Err(contest.id.unwrap())) } } } @@ -1658,7 +1659,7 @@ pub fn admin_show_participation<T: MedalConnection>(conn: &T, user_id: i32, cont } } - let contest = conn.get_contest_by_id_complete(contest_id); + let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?; #[rustfmt::skip] let subms: Vec<(String, Vec<(i32, Vec<(String, i32)>)>)> = @@ -1721,7 +1722,7 @@ pub fn admin_delete_participation<T: MedalConnection>(conn: &T, user_id: i32, co let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?; let _part = conn.get_participation(user.id, contest_id).ok_or(MedalError::AccessDenied)?; - let contest = conn.get_contest_by_id_complete(contest_id); + let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?; if !session.is_admin() { // Check access for teachers @@ -1771,7 +1772,7 @@ pub fn admin_contest_export<T: MedalConnection>(conn: &T, contest_id: i32, sessi .ensure_admin() .ok_or(MedalError::AccessDenied)?; - let contest = conn.get_contest_by_id_complete(contest_id); + let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?; let taskgroup_ids: Vec<(i32, String)> = contest.taskgroups.into_iter().map(|tg| (tg.id.unwrap(), tg.name)).collect(); diff --git a/src/db_conn.base.rs b/src/db_conn.base.rs index 94c11e01..01f0bc8e 100644 --- a/src/db_conn.base.rs +++ b/src/db_conn.base.rs @@ -1065,7 +1065,7 @@ impl MedalConnection for Connection { .unwrap() } - fn get_contest_by_id(&self, contest_id: i32) -> Contest { + fn get_contest_by_id(&self, contest_id: i32) -> Option<Contest> { let query = "SELECT location, filename, name, duration, public, start_date, end_date, review_start_date, review_end_date, min_grade, max_grade, protected, requires_login, requires_contest, secret, message @@ -1091,10 +1091,9 @@ impl MedalConnection for Connection { message: row.get(15), taskgroups: Vec::new() }) .unwrap() - .unwrap() // TODO: Should return Option? } - fn get_contest_by_id_complete(&self, contest_id: i32) -> Contest { + fn get_contest_by_id_complete(&self, contest_id: i32) -> Option<Contest> { let query = "SELECT contest.location, contest.filename, contest.name, contest.duration, contest.public, contest.start_date, contest.end_date, contest.review_start_date, contest.review_end_date, contest.min_grade, contest.max_grade, contest.protected, contest.requires_login, @@ -1150,7 +1149,7 @@ impl MedalConnection for Connection { taskgroup.tasks.push(t); } contest.taskgroups.push(taskgroup); - contest + Some(contest) } else { // If the contest has no tasks, we fall back to the function, that does not try to gather the task // information @@ -1158,7 +1157,7 @@ impl MedalConnection for Connection { } } - fn get_contest_by_id_partial(&self, contest_id: i32) -> Contest { + fn get_contest_by_id_partial(&self, contest_id: i32) -> Option<Contest> { let query = "SELECT contest.location, contest.filename, contest.name, contest.duration, contest.public, contest.start_date, contest.end_date, contest.review_start_date, contest.review_end_date, contest.min_grade, contest.max_grade, contest.protected, contest.requires_login, @@ -1198,13 +1197,19 @@ impl MedalConnection for Connection { .unwrap(); let mut taskgroupcontest_iter = taskgroupcontest.into_iter(); - let (mut contest, taskgroup) = taskgroupcontest_iter.next().unwrap(); - contest.taskgroups.push(taskgroup); - for tgc in taskgroupcontest_iter { - let (_, tg) = tgc; - contest.taskgroups.push(tg); + if let Some((mut contest, taskgroup)) = taskgroupcontest_iter.next() { + contest.taskgroups.push(taskgroup); + for tgc in taskgroupcontest_iter { + let (_, tg) = tgc; + contest.taskgroups.push(tg); + } + Some(contest) + } + else { + // If the contest has no tasks, we fall back to the function, that does not try to gather the task + // information + self.get_contest_by_id(contest_id) } - contest } fn get_participation(&self, session_id: i32, contest_id: i32) -> Option<Participation> { @@ -1293,7 +1298,7 @@ impl MedalConnection for Connection { } } } - fn get_task_by_id(&self, task_id: i32) -> Task { + fn get_task_by_id(&self, task_id: i32) -> Option<Task> { let query = "SELECT location, stars, taskgroup FROM task WHERE id = $1"; @@ -1302,9 +1307,8 @@ impl MedalConnection for Connection { location: row.get(0), stars: row.get(1) }) .unwrap() - .unwrap() } - fn get_task_by_id_complete(&self, task_id: i32) -> (Task, Taskgroup, Contest) { + fn get_task_by_id_complete(&self, task_id: i32) -> Option<(Task, Taskgroup, Contest)> { let query = "SELECT task.location, task.stars, taskgroup.id, taskgroup.name, taskgroup.active, contest.id, contest.location, contest.filename, contest.name, contest.duration, contest.public, contest.start_date, contest.end_date, contest.review_start_date, contest.review_end_date, @@ -1343,7 +1347,6 @@ impl MedalConnection for Connection { taskgroups: Vec::new() }) }) .unwrap() - .unwrap() } fn get_submission_to_validate(&self, tasklocation: &str, subtask: Option<&str>) -> i32 { diff --git a/src/db_conn.rs b/src/db_conn.rs index 6210f324..759849fe 100644 --- a/src/db_conn.rs +++ b/src/db_conn.rs @@ -99,15 +99,15 @@ pub trait MedalConnection { /// Returns the contest identified by `contest_id` without any associated taskgroups. Panics if the contest does not /// exist. - fn get_contest_by_id(&self, contest_id: i32) -> Contest; + fn get_contest_by_id(&self, contest_id: i32) -> Option<Contest>; /// Returns the contest identified by `contest_id` with associated taskgroups but without any associated tasks of /// the taskgroups. Panics if the contest does not exist. - fn get_contest_by_id_partial(&self, contest_id: i32) -> Contest; + fn get_contest_by_id_partial(&self, contest_id: i32) -> Option<Contest>; /// Returns the contest identified by `contest_id` with associated taskgroups and all associated tasks of the /// taskgroups. Panics if the contest does not exist. - fn get_contest_by_id_complete(&self, contest_id: i32) -> Contest; + fn get_contest_by_id_complete(&self, contest_id: i32) -> Option<Contest>; /// Try to get the participation associated to the session id `session_id` and the contest id `contest_id`. /// @@ -132,8 +132,8 @@ pub trait MedalConnection { /// Returns an `Result` that either contains the new `Participation` if the checks succeded or no value if the /// checks failed. fn new_participation(&self, session: &str, contest_id: i32) -> Result<Participation, ()>; - fn get_task_by_id(&self, task_id: i32) -> Task; - fn get_task_by_id_complete(&self, task_id: i32) -> (Task, Taskgroup, Contest); + fn get_task_by_id(&self, task_id: i32) -> Option<Task>; + fn get_task_by_id_complete(&self, task_id: i32) -> Option<(Task, Taskgroup, Contest)>; fn get_submission_to_validate(&self, tasklocation: &str, subtask: Option<&str>) -> i32; fn find_next_submission_to_validate(&self, userid: i32, taskgroupid: i32); diff --git a/src/db_conn_postgres.rs b/src/db_conn_postgres.rs index fe6c5ad7..7cf34137 100644 --- a/src/db_conn_postgres.rs +++ b/src/db_conn_postgres.rs @@ -1158,7 +1158,7 @@ impl MedalConnection for Connection { .unwrap() } - fn get_contest_by_id(&self, contest_id: i32) -> Contest { + fn get_contest_by_id(&self, contest_id: i32) -> Option<Contest> { let query = "SELECT location, filename, name, duration, public, start_date, end_date, review_start_date, review_end_date, min_grade, max_grade, protected, requires_login, requires_contest, secret, message @@ -1184,10 +1184,9 @@ impl MedalConnection for Connection { message: row.get(15), taskgroups: Vec::new() }) .unwrap() - .unwrap() // TODO: Should return Option? } - fn get_contest_by_id_complete(&self, contest_id: i32) -> Contest { + fn get_contest_by_id_complete(&self, contest_id: i32) -> Option<Contest> { let query = "SELECT contest.location, contest.filename, contest.name, contest.duration, contest.public, contest.start_date, contest.end_date, contest.review_start_date, contest.review_end_date, contest.min_grade, contest.max_grade, contest.protected, contest.requires_login, @@ -1243,7 +1242,7 @@ impl MedalConnection for Connection { taskgroup.tasks.push(t); } contest.taskgroups.push(taskgroup); - contest + Some(contest) } else { // If the contest has no tasks, we fall back to the function, that does not try to gather the task // information @@ -1251,7 +1250,7 @@ impl MedalConnection for Connection { } } - fn get_contest_by_id_partial(&self, contest_id: i32) -> Contest { + fn get_contest_by_id_partial(&self, contest_id: i32) -> Option<Contest> { let query = "SELECT contest.location, contest.filename, contest.name, contest.duration, contest.public, contest.start_date, contest.end_date, contest.review_start_date, contest.review_end_date, contest.min_grade, contest.max_grade, contest.protected, contest.requires_login, @@ -1291,13 +1290,18 @@ impl MedalConnection for Connection { .unwrap(); let mut taskgroupcontest_iter = taskgroupcontest.into_iter(); - let (mut contest, taskgroup) = taskgroupcontest_iter.next().unwrap(); - contest.taskgroups.push(taskgroup); - for tgc in taskgroupcontest_iter { - let (_, tg) = tgc; - contest.taskgroups.push(tg); + if let Some((mut contest, taskgroup)) = taskgroupcontest_iter.next() { + contest.taskgroups.push(taskgroup); + for tgc in taskgroupcontest_iter { + let (_, tg) = tgc; + contest.taskgroups.push(tg); + } + Some(contest) + } else { + // If the contest has no tasks, we fall back to the function, that does not try to gather the task + // information + self.get_contest_by_id(contest_id) } - contest } fn get_participation(&self, session_id: i32, contest_id: i32) -> Option<Participation> { @@ -1386,7 +1390,7 @@ impl MedalConnection for Connection { } } } - fn get_task_by_id(&self, task_id: i32) -> Task { + fn get_task_by_id(&self, task_id: i32) -> Option<Task> { let query = "SELECT location, stars, taskgroup FROM task WHERE id = $1"; @@ -1395,9 +1399,8 @@ impl MedalConnection for Connection { location: row.get(0), stars: row.get(1) }) .unwrap() - .unwrap() } - fn get_task_by_id_complete(&self, task_id: i32) -> (Task, Taskgroup, Contest) { + fn get_task_by_id_complete(&self, task_id: i32) -> Option<(Task, Taskgroup, Contest)> { let query = "SELECT task.location, task.stars, taskgroup.id, taskgroup.name, taskgroup.active, contest.id, contest.location, contest.filename, contest.name, contest.duration, contest.public, contest.start_date, contest.end_date, contest.review_start_date, contest.review_end_date, @@ -1436,7 +1439,6 @@ impl MedalConnection for Connection { taskgroups: Vec::new() }) }) .unwrap() - .unwrap() } fn get_submission_to_validate(&self, tasklocation: &str, subtask: Option<&str>) -> i32 { diff --git a/src/db_conn_sqlite_new.rs b/src/db_conn_sqlite_new.rs index 0ddf8305..4f7ba2e4 100644 --- a/src/db_conn_sqlite_new.rs +++ b/src/db_conn_sqlite_new.rs @@ -1158,7 +1158,7 @@ impl MedalConnection for Connection { .unwrap() } - fn get_contest_by_id(&self, contest_id: i32) -> Contest { + fn get_contest_by_id(&self, contest_id: i32) -> Option<Contest> { let query = "SELECT location, filename, name, duration, public, start_date, end_date, review_start_date, review_end_date, min_grade, max_grade, protected, requires_login, requires_contest, secret, message @@ -1184,10 +1184,9 @@ impl MedalConnection for Connection { message: row.get(15), taskgroups: Vec::new() }) .unwrap() - .unwrap() // TODO: Should return Option? } - fn get_contest_by_id_complete(&self, contest_id: i32) -> Contest { + fn get_contest_by_id_complete(&self, contest_id: i32) -> Option<Contest> { let query = "SELECT contest.location, contest.filename, contest.name, contest.duration, contest.public, contest.start_date, contest.end_date, contest.review_start_date, contest.review_end_date, contest.min_grade, contest.max_grade, contest.protected, contest.requires_login, @@ -1243,7 +1242,7 @@ impl MedalConnection for Connection { taskgroup.tasks.push(t); } contest.taskgroups.push(taskgroup); - contest + Some(contest) } else { // If the contest has no tasks, we fall back to the function, that does not try to gather the task // information @@ -1251,7 +1250,7 @@ impl MedalConnection for Connection { } } - fn get_contest_by_id_partial(&self, contest_id: i32) -> Contest { + fn get_contest_by_id_partial(&self, contest_id: i32) -> Option<Contest> { let query = "SELECT contest.location, contest.filename, contest.name, contest.duration, contest.public, contest.start_date, contest.end_date, contest.review_start_date, contest.review_end_date, contest.min_grade, contest.max_grade, contest.protected, contest.requires_login, @@ -1291,13 +1290,18 @@ impl MedalConnection for Connection { .unwrap(); let mut taskgroupcontest_iter = taskgroupcontest.into_iter(); - let (mut contest, taskgroup) = taskgroupcontest_iter.next().unwrap(); - contest.taskgroups.push(taskgroup); - for tgc in taskgroupcontest_iter { - let (_, tg) = tgc; - contest.taskgroups.push(tg); + if let Some((mut contest, taskgroup)) = taskgroupcontest_iter.next() { + contest.taskgroups.push(taskgroup); + for tgc in taskgroupcontest_iter { + let (_, tg) = tgc; + contest.taskgroups.push(tg); + } + Some(contest) + } else { + // If the contest has no tasks, we fall back to the function, that does not try to gather the task + // information + self.get_contest_by_id(contest_id) } - contest } fn get_participation(&self, session_id: i32, contest_id: i32) -> Option<Participation> { @@ -1386,7 +1390,7 @@ impl MedalConnection for Connection { } } } - fn get_task_by_id(&self, task_id: i32) -> Task { + fn get_task_by_id(&self, task_id: i32) -> Option<Task> { let query = "SELECT location, stars, taskgroup FROM task WHERE id = ?1"; @@ -1395,9 +1399,8 @@ impl MedalConnection for Connection { location: row.get(0), stars: row.get(1) }) .unwrap() - .unwrap() } - fn get_task_by_id_complete(&self, task_id: i32) -> (Task, Taskgroup, Contest) { + fn get_task_by_id_complete(&self, task_id: i32) -> Option<(Task, Taskgroup, Contest)> { let query = "SELECT task.location, task.stars, taskgroup.id, taskgroup.name, taskgroup.active, contest.id, contest.location, contest.filename, contest.name, contest.duration, contest.public, contest.start_date, contest.end_date, contest.review_start_date, contest.review_end_date, @@ -1436,7 +1439,6 @@ impl MedalConnection for Connection { taskgroups: Vec::new() }) }) .unwrap() - .unwrap() } fn get_submission_to_validate(&self, tasklocation: &str, subtask: Option<&str>) -> i32 { diff --git a/src/webfw_iron.rs b/src/webfw_iron.rs index 5a668800..50e27f55 100644 --- a/src/webfw_iron.rs +++ b/src/webfw_iron.rs @@ -305,6 +305,9 @@ impl<'c, 'a, 'b> From<AugMedalError<'c, 'a, 'b>> for IronError { core::MedalError::AccessDenied => IronError { error: Box::new(SessionError { message: "Access denied".to_string() }), response: Response::with(status::Unauthorized) }, + core::MedalError::UnknownId => IronError { error: Box::new(SessionError { message: + "Not found".to_string() }), + response: Response::with(status::NotFound) }, core::MedalError::CsrfCheckFailed => IronError { error: Box::new(SessionError { message: "CSRF Error".to_string() }), response: Response::with(status::Forbidden) }, @@ -757,7 +760,7 @@ fn task<C>(req: &mut Request) -> IronResult<Response> config.auto_save_interval.unwrap_or(10) }; - match with_conn![core::show_task, C, req, task_id, &session_token, autosaveinterval] { + match with_conn![core::show_task, C, req, task_id, &session_token, autosaveinterval].aug(req)? { Ok((template, data)) => { let mut resp = Response::new(); resp.set_mut(Template::new(&template, data)).set_mut(status::Ok); diff --git a/templates/jwinf/index.hbs b/templates/jwinf/index.hbs index fb4c6d91..04042713 100644 --- a/templates/jwinf/index.hbs +++ b/templates/jwinf/index.hbs @@ -30,7 +30,7 @@ {{#if admin}} <div class="columns alogin"> <div class="column is-8 is-offset-2"> - <a href="/admin/"> + <a href="/admin"> <div class="notification is-primary"> <h3 class="title is-5">Adminstration</h3> <p>Administrationsseite öffnen und Benutzer, Gruppen und Teilnahmen verwalten</p> -- GitLab