medal/
core.rs

1/*  medal                                                                                                            *\
2 *  Copyright (C) 2022  Bundesweite Informatikwettbewerbe, Robert Czechowski                                         *
3 *                                                                                                                   *
4 *  This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero        *
5 *  General Public License as published  by the Free Software Foundation, either version 3 of the License, or (at    *
6 *  your option) any later version.                                                                                  *
7 *                                                                                                                   *
8 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the       *
9 *  implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public      *
10 *  License for more details.                                                                                        *
11 *                                                                                                                   *
12 *  You should have received a copy of the GNU Affero General Public License along with this program.  If not, see   *
13\*  <http://www.gnu.org/licenses/>.                                                                                  */
14
15use time;
16
17use config::OauthProvider;
18use db_conn::MedalConnection;
19#[cfg(feature = "signup")]
20use db_conn::SignupResult;
21use db_objects::OptionSession;
22use db_objects::SessionUser;
23use db_objects::{Contest, Grade, Group, Participation, Submission, Taskgroup};
24use helpers;
25use webauthn_rs::prelude as webauthn;
26use webfw_iron::{json_val, to_json};
27
28#[derive(Serialize, Deserialize)]
29pub struct SubTaskInfo {
30    pub id: i32,
31    pub linktext: String,
32    pub active: bool,
33    pub greyout: bool,
34}
35
36#[derive(Serialize, Deserialize)]
37pub struct TaskInfo {
38    pub name: String,
39    pub subtasks: Vec<SubTaskInfo>,
40}
41
42#[derive(Clone, Serialize, Deserialize)]
43pub struct ContestInfo {
44    pub id: i32,
45    pub name: String,
46    pub duration: i32,
47    pub public: bool,
48    pub requires_login: bool,
49    pub image: Option<String>,
50    pub language: Option<String>,
51    pub category: Option<String>,
52    pub team_participation: bool,
53    pub tags: Vec<String>,
54}
55
56#[derive(Clone, Debug)]
57pub enum MedalError {
58    NotLoggedIn,
59    AccessDenied,
60    CsrfCheckFailed,
61    SessionTimeout,
62    DatabaseError,
63    ConfigurationError,
64    DatabaseConnectionError,
65    PasswordHashingError,
66    UnmatchedPasswords,
67    NotFound,
68    AccountIncomplete,
69    UnknownId,
70    OauthError(String),
71    WebauthnError,
72    ErrorWithJson(json_val::Map<String, json_val::Value>),
73}
74
75pub struct LoginInfo {
76    pub password_login: bool,
77    pub self_url: Option<String>,
78    pub oauth_providers: Option<Vec<OauthProvider>>,
79}
80
81type MedalValue = (String, json_val::Map<String, json_val::Value>);
82type MedalResult<T> = Result<T, MedalError>;
83type MedalValueResult = MedalResult<MedalValue>;
84
85type JsonValue = json_val::Map<String, json_val::Value>;
86type JsonValueResult = MedalResult<JsonValue>;
87
88fn fill_user_data_prefix(session: &SessionUser, data: &mut json_val::Map<String, serde_json::Value>, prefix: &str) {
89    data.insert(prefix.to_string() + "username", to_json(&session.username));
90    data.insert(prefix.to_string() + "firstname", to_json(&session.firstname));
91    data.insert(prefix.to_string() + "lastname", to_json(&session.lastname));
92    data.insert(prefix.to_string() + "teacher", to_json(&session.is_teacher));
93    data.insert(prefix.to_string() + "is_teacher", to_json(&session.is_teacher));
94    data.insert(prefix.to_string() + "teacher_schoolname", to_json(&session.school_name));
95    data.insert(prefix.to_string() + "admin", to_json(&session.is_admin));
96    data.insert(prefix.to_string() + "is_admin", to_json(&session.is_admin));
97    data.insert(prefix.to_string() + "logged_in", to_json(&session.is_logged_in()));
98    data.insert(prefix.to_string() + "csrf_token", to_json(&session.csrf_token));
99    data.insert(prefix.to_string() + "sex",
100                to_json(&(match session.sex {
101                            Some(0) | None => "/",
102                            Some(1) => "m",
103                            Some(2) => "w",
104                            Some(3) => "d",
105                            Some(4) => "…",
106                            _ => "?",
107                        })));
108}
109
110fn fill_user_data(session: &SessionUser, data: &mut json_val::Map<String, serde_json::Value>) {
111    fill_user_data_prefix(session, data, "");
112
113    data.insert("parent".to_string(), to_json(&"base"));
114    data.insert("medal_version".to_string(), to_json(&env!("GIT_VERSION")));
115}
116
117fn fill_oauth_data(login_info: LoginInfo, data: &mut json_val::Map<String, serde_json::Value>) {
118    let mut oauth_links: Vec<(String, String, String)> = Vec::new();
119    if let Some(oauth_providers) = login_info.oauth_providers {
120        for oauth_provider in oauth_providers {
121            oauth_links.push((oauth_provider.provider_id.to_owned(),
122                              oauth_provider.login_link_text.to_owned(),
123                              oauth_provider.url.to_owned()));
124        }
125    }
126
127    data.insert("self_url".to_string(), to_json(&login_info.self_url));
128    data.insert("oauth_links".to_string(), to_json(&oauth_links));
129
130    data.insert("password_login".to_string(), to_json(&login_info.password_login));
131}
132
133fn grade_to_string(grade: i32) -> String {
134    match grade {
135        0 => "Noch kein Schüler".to_string(),
136        n @ 1..=10 => format!("{}", n),
137        11 => "11 (G8)".to_string(),
138        12 => "12 (G8)".to_string(),
139        111 => "11 (G9)".to_string(),
140        112 => "12 (G9)".to_string(),
141        113 => "13 (G9)".to_string(),
142        114 => "Berufsschule".to_string(),
143        255 => "Kein Schüler mehr".to_string(),
144        _ => "?".to_string(),
145    }
146}
147
148fn common_prefix<'a>(a: &'a str, b: &'a str) -> &'a str {
149    let mut ac = a.char_indices();
150    let mut bc = b.char_indices();
151
152    let mut ai;
153    let mut bi;
154    loop {
155        ai = ac.next();
156        bi = bc.next();
157        if ai != bi {
158            break;
159        }
160        if ai.is_none() {
161            break;
162        }
163    }
164    if ai.is_none() {
165        return a;
166    }
167    if bi.is_none() {
168        return b;
169    }
170
171    a.get(..(ai.unwrap().0)).unwrap()
172}
173
174pub fn index<T: MedalConnection>(conn: &T, session_token: Option<String>, login_info: LoginInfo) -> MedalValueResult {
175    let mut data = json_val::Map::new();
176
177    if let Some(token) = session_token {
178        if let Some(session) = conn.get_session(&token) {
179            fill_user_data(&session, &mut data);
180
181            if session.logincode.is_some() && session.firstname.is_none() {
182                return Err(MedalError::AccountIncomplete);
183            }
184        }
185    }
186
187    fill_oauth_data(login_info, &mut data);
188
189    let now = time::get_time();
190    let contest_list = conn.get_contest_list();
191    let mut contests_running: Vec<(String, String, (i64, i64))> =
192        contest_list.iter()
193                    .filter(|c| c.public)
194                    .filter(|c| c.duration != 0 || c.category == Some("contest".to_string()))
195                    .filter(|c| !c.requires_login.unwrap_or(false) || c.category.is_some())
196                    .filter(|c| c.end.map(|end| now <= end).unwrap_or(false))
197                    .filter(|c| c.start.map(|start| now >= start).unwrap_or(true))
198                    .map(|c| {
199                        (c.name.clone(),
200                         if let Some(cat) = c.category.as_ref() {
201                             format!("{}/{}", cat, c.id.unwrap_or(0))
202                         } else {
203                             format!("{}", c.id.unwrap_or(0))
204                         },
205                         {
206                             let t = c.end.unwrap().sec - now.sec;
207                             (t / 60 / 60 / 24, t / 60 / 60 % 24)
208                         })
209                    })
210                    .collect();
211
212    let mut i = 0;
213    while i < contests_running.len() {
214        let mut count = 0;
215        while i + 1 < contests_running.len() {
216            if contests_running[i].2 == contests_running[i + 1].2 {
217                // Same end date
218
219                contests_running[i].0 = common_prefix(&contests_running[i].0, &contests_running[i + 1].0).to_string();
220                count += 1;
221                contests_running.remove(i + 1);
222            } else {
223                break;
224            }
225        }
226        if count > 0 {
227            contests_running[i].0 = format!("{}… ({}x)", contests_running[i].0, count + 1);
228            contests_running[i].1 = "?filter=current".to_string();
229        }
230        i += 1;
231    }
232
233    if contests_running.len() > 0 {
234        data.insert("current_contests".to_string(), to_json(&contests_running));
235    }
236
237    let mut contests_comming: Vec<(String, String, (i64, i64))> =
238        contest_list.iter()
239                    .filter(|c| c.public)
240                    .filter(|c| c.duration != 0 || c.category == Some("contest".to_string()))
241                    .filter(|c| !c.requires_login.unwrap_or(false) || c.category.is_some())
242                    .filter(|c| c.start.map(|start| now <= start).unwrap_or(false))
243                    .map(|c| {
244                        (c.name.clone(),
245                         if let Some(cat) = c.category.as_ref() {
246                             format!("{}/{}", cat, c.id.unwrap_or(0))
247                         } else {
248                             format!("{}", c.id.unwrap_or(0))
249                         },
250                         {
251                             let t = c.start.unwrap().sec - now.sec;
252                             (t / 60 / 60 / 24, t / 60 / 60 % 24)
253                         })
254                    })
255                    .collect();
256
257    let mut i = 0;
258    while i < contests_comming.len() {
259        let mut count = 0;
260        while i + 1 < contests_comming.len() {
261            if contests_comming[i].2 == contests_comming[i + 1].2 {
262                // Same end date
263
264                contests_comming[i].0 = common_prefix(&contests_comming[i].0, &contests_comming[i + 1].0).to_string();
265                count += 1;
266                contests_comming.remove(i + 1);
267            } else {
268                break;
269            }
270        }
271        if count > 0 {
272            contests_comming[i].0 = format!("{}… ({}x)", contests_comming[i].0, count + 1);
273            contests_comming[i].1 = "?filter=current".to_string();
274        }
275        i += 1;
276    }
277
278    if contests_comming.len() > 0 {
279        data.insert("upcoming_contests".to_string(), to_json(&contests_comming));
280    }
281
282    data.insert("parent".to_string(), to_json(&"base"));
283    data.insert("index".to_string(), to_json(&true));
284    Ok(("index".to_owned(), data))
285}
286
287pub fn show_login<T: MedalConnection>(conn: &T, session_token: Option<String>, login_info: LoginInfo)
288                                      -> (String, json_val::Map<String, json_val::Value>) {
289    let mut data = json_val::Map::new();
290
291    if let Some(token) = session_token {
292        if let Some(session) = conn.get_session(&token) {
293            fill_user_data(&session, &mut data);
294        }
295    }
296
297    fill_oauth_data(login_info, &mut data);
298
299    data.insert("parent".to_string(), to_json(&"base"));
300    ("login".to_owned(), data)
301}
302
303pub fn status<T: MedalConnection>(conn: &T, config_secret: Option<String>, given_secret: Option<String>)
304                                  -> MedalResult<String> {
305    if config_secret == given_secret {
306        Ok(conn.get_debug_information())
307    } else {
308        Err(MedalError::AccessDenied)
309    }
310}
311
312pub fn debug<T: MedalConnection>(conn: &T, session_token: Option<String>)
313                                 -> (String, json_val::Map<String, json_val::Value>) {
314    let mut data = json_val::Map::new();
315
316    if let Some(token) = session_token {
317        if let Some(session) = conn.get_session(&token) {
318            data.insert("known_session".to_string(), to_json(&true));
319            data.insert("session_id".to_string(), to_json(&session.id));
320            data.insert("now_timestamp".to_string(), to_json(&time::get_time().sec));
321            if let Some(last_activity) = session.last_activity {
322                data.insert("session_timestamp".to_string(), to_json(&last_activity.sec));
323                data.insert("timediff".to_string(), to_json(&(time::get_time() - last_activity).num_seconds()));
324            }
325            if session.is_alive() {
326                data.insert("alive_session".to_string(), to_json(&true));
327                if session.is_logged_in() {
328                    data.insert("logged_in".to_string(), to_json(&true));
329                    data.insert("username".to_string(), to_json(&session.username));
330                    data.insert("firstname".to_string(), to_json(&session.firstname));
331                    data.insert("lastname".to_string(), to_json(&session.lastname));
332                    data.insert("teacher".to_string(), to_json(&session.is_teacher));
333                    data.insert("oauth_provider".to_string(), to_json(&session.oauth_provider));
334                    data.insert("oauth_id".to_string(), to_json(&session.oauth_foreign_id));
335                    data.insert("logincode".to_string(), to_json(&session.logincode));
336                    data.insert("managed_by".to_string(), to_json(&session.managed_by));
337                }
338            }
339        }
340        data.insert("session".to_string(), to_json(&token));
341    } else {
342        data.insert("session".to_string(), to_json(&"No session token given"));
343    }
344
345    ("debug".to_owned(), data)
346}
347
348pub fn debug_create_session<T: MedalConnection>(conn: &T, session_token: Option<String>) {
349    if let Some(token) = session_token {
350        conn.get_session_or_new(&token).unwrap();
351    }
352}
353
354#[derive(PartialEq, Eq, Debug)]
355pub enum ContestVisibility {
356    All,
357    Open,
358    Current,
359    LoginRequired,
360    StandaloneTask,
361    WithSecret(String),
362}
363
364pub fn show_contests<T: MedalConnection>(conn: &T, session_token: &str, category: Option<String>,
365                                         login_info: LoginInfo, visibility: ContestVisibility, is_results: bool)
366                                         -> MedalValueResult {
367    let mut data = json_val::Map::new();
368
369    let session = conn.get_session_or_new(&session_token).map_err(|_| MedalError::DatabaseConnectionError)?;
370    fill_user_data(&session, &mut data);
371
372    if session.is_logged_in() {
373        data.insert("can_start".to_string(), to_json(&true));
374    }
375
376    fill_oauth_data(login_info, &mut data);
377
378    let contest_list = if is_results {
379        conn.get_contest_list_with_group_member_participations(session.id)
380    } else {
381        conn.get_contest_list()
382    };
383
384    let now = time::get_time();
385    let v: Vec<ContestInfo> =
386        contest_list.iter()
387                    .filter(|c| category.as_ref().map(|cat| c.category.as_ref() == Some(cat)).unwrap_or(true))
388                    .filter(|c| c.public || matches!(visibility, ContestVisibility::WithSecret(_)))
389                    .filter(|c| {
390                        if let ContestVisibility::WithSecret(secret) = &visibility {
391                            c.secret.as_ref() == Some(secret)
392                        } else {
393                            true
394                        }
395                    })
396                    .filter(|c| {
397                        (!c.standalone_task.unwrap_or(false))
398                        || visibility == ContestVisibility::StandaloneTask
399                        || category == Some("standalone_task".to_string())
400                    })
401                    .filter(|c| c.standalone_task.unwrap_or(false) || visibility != ContestVisibility::StandaloneTask)
402                    .filter(|c| {
403                        c.end.map(|end| now <= end).unwrap_or(true)
404                        || (visibility == ContestVisibility::All && (c.category.is_none() || is_results))
405                    })
406                    .filter(|c| c.duration == 0 || visibility != ContestVisibility::Open)
407                    .filter(|c| c.duration != 0 || visibility != ContestVisibility::Current)
408                    .filter(|c| c.requires_login.unwrap_or(false) || visibility != ContestVisibility::LoginRequired)
409                    .filter(|c| {
410                        !c.requires_login.unwrap_or(false)
411                        || visibility == ContestVisibility::LoginRequired
412                        || visibility == ContestVisibility::All
413                    })
414                    .map(|c| ContestInfo { id: c.id.unwrap(),
415                                           name: c.name.clone(),
416                                           duration: c.duration,
417                                           public: c.public,
418                                           requires_login: c.requires_login.unwrap_or(false),
419                                           image: c.image.as_ref().map(|i| format!("/{}{}", c.location, i)),
420                                           language: c.language.clone(),
421                                           category: c.category.clone(),
422                                           team_participation: false,
423                                           tags: c.tags.clone() })
424                    .collect();
425
426    if category.is_some() {
427        data.insert("contests".to_string(), to_json(&v));
428    } else {
429        let contests_training: Vec<ContestInfo> =
430            v.clone().into_iter().filter(|c| !c.requires_login).filter(|c| c.duration == 0).collect();
431        let contests_contest: Vec<ContestInfo> =
432            v.clone().into_iter().filter(|c| !c.requires_login).filter(|c| c.duration != 0).collect();
433        let contests_challenge: Vec<ContestInfo> = v.into_iter().filter(|c| c.requires_login).collect();
434
435        data.insert("contests_training".to_string(), to_json(&contests_training));
436        data.insert("contests_contest".to_string(), to_json(&contests_contest));
437        data.insert("contests_challenge".to_string(), to_json(&contests_challenge));
438
439        data.insert("contests_training_header".to_string(), to_json(&"Trainingsaufgaben"));
440        data.insert("contests_contest_header".to_string(), to_json(&"Wettbewerbe"));
441        data.insert("contests_challenge_header".to_string(), to_json(&"Herausforderungen"));
442
443        if visibility == ContestVisibility::StandaloneTask {
444            data.insert("contests_training_header".to_string(), to_json(&"Einzelne Aufgaben ohne Wertung"));
445        }
446    }
447
448    if let ContestVisibility::WithSecret(secret) = visibility {
449        data.insert("secret".to_string(), to_json(&secret));
450        data.insert("has_secret".to_string(), to_json(&true));
451    }
452
453    Ok(("contests".to_owned(), data))
454}
455
456fn generate_subtaskstars(tg: &Taskgroup, grade: &Grade, ast: Option<i32>) -> Vec<SubTaskInfo> {
457    let mut subtaskinfos = Vec::new();
458    let mut not_print_yet = true;
459    for st in &tg.tasks {
460        let mut blackstars: usize = 0;
461        if not_print_yet && st.stars >= grade.grade.unwrap_or(0) {
462            blackstars = grade.grade.unwrap_or(0) as usize;
463            not_print_yet = false;
464        }
465
466        let greyout = not_print_yet && st.stars < grade.grade.unwrap_or(0);
467        let active = ast.is_some() && st.id == ast;
468        let linktext = format!("{}{}",
469                               str::repeat("★", blackstars as usize),
470                               str::repeat("☆", st.stars as usize - blackstars as usize));
471        let si = SubTaskInfo { id: st.id.unwrap(), linktext, active, greyout };
472
473        subtaskinfos.push(si);
474    }
475    subtaskinfos
476}
477
478#[derive(Serialize, Deserialize)]
479pub struct ContestStartConstraints {
480    pub contest_not_begun: bool,
481    pub contest_over: bool,
482    pub contest_running: bool,
483    pub grade_too_low: bool,
484    pub grade_too_high: bool,
485    pub grade_matching: bool,
486}
487
488fn check_contest_qualification<T: MedalConnection>(conn: &T, session: &SessionUser, contest: &Contest) -> Option<bool> {
489    // Produced by `config.requires_contest.map(|list| list.join(",")),` in contestreader_yaml.rs
490    let required_contests = contest.requires_contest.as_ref()?.split(',');
491
492    for req_contest in required_contests {
493        if conn.has_participation_by_contest_file(session.id, &contest.location, req_contest) {
494            return Some(true);
495        }
496    }
497
498    Some(false)
499}
500
501fn check_contest_constraints(session: &SessionUser, contest: &Contest) -> ContestStartConstraints {
502    let now = time::get_time();
503    let student_grade = session.grade % 100 - if session.grade / 100 == 1 { 1 } else { 0 };
504
505    let contest_not_begun = contest.start.map(|start| now < start).unwrap_or(false);
506    let contest_over = contest.end.map(|end| now > end).unwrap_or(false);
507    let grade_too_low =
508        contest.min_grade.map(|min_grade| student_grade < min_grade && !session.is_teacher).unwrap_or(false);
509    let grade_too_high =
510        contest.max_grade.map(|max_grade| student_grade > max_grade && !session.is_teacher).unwrap_or(false);
511
512    let contest_running = !contest_not_begun && !contest_over;
513    let grade_matching = !grade_too_low && !grade_too_high;
514
515    ContestStartConstraints { contest_not_begun,
516                              contest_over,
517                              contest_running,
518                              grade_too_low,
519                              grade_too_high,
520                              grade_matching }
521}
522
523#[derive(Serialize, Deserialize)]
524pub struct ContestTimeInfo {
525    pub passed_secs_total: i64,
526    pub left_secs_total: i64,
527    pub left_mins_total: i64,
528    pub left_hour: i64,
529    pub left_min: i64,
530    pub left_sec: i64,
531    pub has_timelimit: bool,
532    pub is_time_left: bool,
533    pub exempt_from_timelimit: bool,
534    pub can_still_compete: bool,
535    pub review_has_timelimit: bool,
536    pub has_future_review: bool,
537    pub has_review_end: bool,
538    pub is_review: bool,
539    pub can_still_compete_or_review: bool,
540
541    pub until_review_start_day: i64,
542    pub until_review_start_hour: i64,
543    pub until_review_start_min: i64,
544
545    pub until_review_end_day: i64,
546    pub until_review_end_hour: i64,
547    pub until_review_end_min: i64,
548}
549
550fn check_contest_time_left(session: &SessionUser, contest: &Contest, participation: &Participation) -> ContestTimeInfo {
551    let now = time::get_time();
552    let passed_secs_total = now.sec - participation.start.sec;
553    if passed_secs_total < 0 {
554        // Handle inconsistent server time
555    }
556    let left_secs_total = i64::from(contest.duration) * 60 - passed_secs_total;
557
558    let is_time_left = contest.duration == 0 || left_secs_total >= 0;
559    let exempt_from_timelimit = session.is_teacher() || session.is_admin();
560
561    let can_still_compete = is_time_left || exempt_from_timelimit;
562
563    let review_has_timelimit = contest.review_end.is_none() && contest.review_start.is_some();
564    let has_future_review = (contest.review_start.is_some() || contest.review_end.is_some())
565                            && contest.review_end.map(|end| end > now).unwrap_or(true);
566    let has_review_end = contest.review_end.is_some();
567    let is_review = !can_still_compete
568                    && (contest.review_start.is_some() || contest.review_end.is_some())
569                    && contest.review_start.map(|start| now >= start).unwrap_or(true)
570                    && contest.review_end.map(|end| now <= end).unwrap_or(true);
571
572    let until_review_start = contest.review_start.map(|start| start.sec - now.sec).unwrap_or(0);
573    let until_review_end = contest.review_end.map(|end| end.sec - now.sec).unwrap_or(0);
574
575    ContestTimeInfo { passed_secs_total,
576                      left_secs_total,
577                      left_mins_total: left_secs_total / 60,
578                      left_hour: left_secs_total / (60 * 60),
579                      left_min: (left_secs_total / 60) % 60,
580                      left_sec: left_secs_total % 60,
581                      has_timelimit: contest.duration != 0,
582                      is_time_left,
583                      exempt_from_timelimit,
584                      can_still_compete,
585                      review_has_timelimit,
586                      has_future_review,
587                      has_review_end,
588                      is_review,
589                      can_still_compete_or_review: can_still_compete || is_review,
590
591                      until_review_start_day: until_review_start / (60 * 60 * 24),
592                      until_review_start_hour: (until_review_start / (60 * 60)) % 24,
593                      until_review_start_min: (until_review_start / 60) % 60,
594
595                      until_review_end_day: until_review_end / (60 * 60 * 24),
596                      until_review_end_hour: (until_review_end / (60 * 60)) % 24,
597                      until_review_end_min: (until_review_end / 60) % 60 }
598}
599
600pub fn show_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str,
601                                        query_string: Option<String>, login_info: LoginInfo, secret: Option<String>)
602                                        -> MedalResult<Result<MedalValue, i32>> {
603    let session = conn.get_session_or_new(&session_token).unwrap();
604
605    if session.logincode.is_some() && session.firstname.is_none() {
606        return Err(MedalError::AccountIncomplete);
607    }
608
609    let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
610
611    let mut opt_part = conn.get_participation(session.id, contest_id);
612    let mut grades = if let Some(part) = opt_part.as_ref() {
613        if let Some(team) = part.team {
614            conn.get_contest_user_grades(team, contest_id)
615        } else {
616            conn.get_contest_user_grades(session.id, contest_id)
617        }
618    } else {
619        conn.get_contest_user_grades(session.id, contest_id)
620    };
621
622    let ci = ContestInfo { id: contest.id.unwrap(),
623                           name: contest.name.clone(),
624                           duration: contest.duration,
625                           public: contest.public,
626                           requires_login: contest.requires_login.unwrap_or(false),
627                           image: None,
628                           language: None,
629                           category: contest.category.clone(),
630                           team_participation: contest.max_teamsize.map(|size| size > 1).unwrap_or(false),
631                           tags: Vec::new() };
632
633    let mut data = json_val::Map::new();
634    data.insert("parent".to_string(), to_json(&"base"));
635    data.insert("empty".to_string(), to_json(&"empty"));
636    data.insert("contest".to_string(), to_json(&ci));
637    data.insert("title".to_string(), to_json(&ci.name));
638    data.insert("message".to_string(), to_json(&contest.message));
639    fill_oauth_data(login_info, &mut data);
640
641    if secret.is_some() && secret != contest.secret {
642        return Err(MedalError::AccessDenied);
643    }
644
645    let has_secret = contest.secret.is_some();
646    let mut require_secret = false;
647    if has_secret {
648        data.insert("secret_field".to_string(), to_json(&true));
649
650        if secret.is_some() {
651            data.insert("secret_field_prefill".to_string(), to_json(&secret));
652        } else {
653            require_secret = true;
654        }
655    }
656
657    let constraints = check_contest_constraints(&session, &contest);
658    let is_qualified = check_contest_qualification(conn, &session, &contest).unwrap_or(true);
659
660    let has_tasks = contest.taskgroups.len() > 0;
661    let can_start = constraints.contest_running
662                    && constraints.grade_matching
663                    && is_qualified
664                    && (has_tasks || has_secret)
665                    && (session.is_logged_in() || contest.secret.is_some() && !contest.requires_login.unwrap_or(false));
666
667    let has_duration = contest.duration > 0;
668
669    data.insert("constraints".to_string(), to_json(&constraints));
670    data.insert("is_qualified".to_string(), to_json(&is_qualified));
671    data.insert("has_duration".to_string(), to_json(&has_duration));
672    data.insert("can_start".to_string(), to_json(&can_start));
673    data.insert("has_tasks".to_string(), to_json(&has_tasks));
674    data.insert("no_tasks".to_string(), to_json(&!has_tasks));
675
676    // Autostart if appropriate
677    // TODO: Should participation start automatically for teacher? Even before the contest start?
678    // Should teachers have all time access or only the same limited amount of time?
679    // if opt_part.is_none() && (contest.duration == 0 || session.is_teacher) {
680    if opt_part.is_none()
681       && contest.duration == 0
682       && constraints.contest_running
683       && constraints.grade_matching
684       && !require_secret
685       && contest.requires_login != Some(true)
686    {
687        conn.new_participation(session.id, contest_id, None).map_err(|_| MedalError::AccessDenied)?;
688        opt_part = Some(Participation { contest: contest_id, user: session.id, start: time::get_time(), team: None });
689    }
690
691    let now = time::get_time();
692    if let Some(start) = contest.start {
693        if now < start {
694            let until = start - now;
695            data.insert("time_until_start".to_string(),
696                        to_json(&[until.num_days(), until.num_hours() % 24, until.num_minutes() % 60]));
697        }
698    }
699
700    if let Some(end) = contest.end {
701        if now < end {
702            let until = end - now;
703            data.insert("time_until_end".to_string(),
704                        to_json(&[until.num_days(), until.num_hours() % 24, until.num_minutes() % 60]));
705        }
706    }
707
708    if session.is_logged_in() {
709        data.insert("logged_in".to_string(), to_json(&true));
710        data.insert("username".to_string(), to_json(&session.username));
711        data.insert("firstname".to_string(), to_json(&session.firstname));
712        data.insert("lastname".to_string(), to_json(&session.lastname));
713        data.insert("teacher".to_string(), to_json(&session.is_teacher));
714        data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
715    }
716
717    if let Some(participation) = opt_part {
718        let time_info = check_contest_time_left(&session, &contest, &participation);
719        data.insert("time_info".to_string(), to_json(&time_info));
720
721        let time_left_formatted =
722            format!("{}:{:02}:{:02}", time_info.left_hour, time_info.left_min, time_info.left_sec);
723        data.insert("time_left_formatted".to_string(), to_json(&time_left_formatted));
724
725        // Is team participation?
726        if let Some(team) = participation.team {
727            // Not relevant for team lead
728            if team != session.id {
729                if time_info.is_time_left {
730                    // Do not allow to participate!
731                    data.insert("block_team_participation".to_string(), to_json(&true));
732                } else {
733                    // Show grades of team participation instead!
734                    grades = conn.get_contest_user_grades(team, contest_id);
735                }
736            }
737        }
738
739        let mut totalgrade = 0;
740        let mut max_totalgrade = 0;
741
742        let mut tasks = Vec::new();
743        for (taskgroup, grade) in contest.taskgroups.into_iter().zip(grades) {
744            let subtaskstars = generate_subtaskstars(&taskgroup, &grade, None);
745            let ti = TaskInfo { name: taskgroup.name, subtasks: subtaskstars };
746            tasks.push(ti);
747
748            totalgrade += grade.grade.unwrap_or(0);
749            max_totalgrade += taskgroup.tasks.iter().map(|x| x.stars).max().unwrap_or(0);
750        }
751        let relative_points = if max_totalgrade > 0 { (totalgrade * 100) / max_totalgrade } else { 0 };
752
753        data.insert("tasks".to_string(), to_json(&tasks));
754
755        data.insert("is_started".to_string(), to_json(&true));
756        data.insert("total_points".to_string(), to_json(&totalgrade));
757        data.insert("max_total_points".to_string(), to_json(&max_totalgrade));
758        data.insert("relative_points".to_string(), to_json(&relative_points));
759        data.insert("lean_page".to_string(), to_json(&true));
760
761        if has_tasks && contest.standalone_task.unwrap_or(false) {
762            return Ok(Err(tasks[0].subtasks[0].id));
763        }
764    }
765
766    if let Some(query_string) = query_string {
767        if !query_string.starts_with("bare") {
768            data.insert("not_bare".to_string(), to_json(&true));
769        }
770
771        if query_string.contains("team_participation=no_logincode") {
772            data.insert("team_error".to_string(), to_json(&"Kein Logincode angegeben"));
773        }
774        if query_string.contains("team_participation=invalid_logincode") {
775            data.insert("team_error".to_string(), to_json(&"Ungültiger Logincode angegeben"));
776        }
777        if query_string.contains("team_participation=own_logincode") {
778            data.insert("team_error".to_string(), to_json(&"Eigener Logincode angegeben"));
779        }
780        if query_string.contains("team_participation=logincode_has_participation") {
781            data.insert("team_error".to_string(), to_json(&"Logincode hat diesen Wettbewerb bereits gestartet"));
782        }
783    } else {
784        data.insert("not_bare".to_string(), to_json(&true));
785    }
786
787    Ok(Ok(("contest".to_owned(), data)))
788}
789
790pub fn show_contest_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
791    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
792    let mut data = json_val::Map::new();
793    fill_user_data(&session, &mut data);
794
795    let (tasknames, resultdata) = conn.get_contest_groups_grades(session.id, contest_id);
796
797    #[derive(Serialize, Deserialize)]
798    struct UserResults {
799        firstname: String,
800        lastname: String,
801        user_id: i32,
802        grade: String,
803        logincode: String,
804        annotation: String,
805        annotation_result: String,
806        results: Vec<String>,
807    }
808
809    #[derive(Serialize, Deserialize)]
810    struct GroupResults {
811        groupname: String,
812        group_id: i32,
813        groupcode: String,
814        user_results: Vec<UserResults>,
815    }
816
817    let mut results: Vec<GroupResults> = Vec::new();
818    let mut has_annotations = false;
819    let mut has_annotations_result = false;
820
821    for (group, groupdata) in resultdata {
822        let mut groupresults: Vec<UserResults> = Vec::new();
823
824        for (user, userdata) in groupdata {
825            let mut userresults: Vec<String> = Vec::new();
826
827            userresults.push(String::new());
828            let mut summe = 0;
829
830            for grade in userdata {
831                if let Some(g) = grade.grade {
832                    userresults.push(format!("{}", g));
833                    summe += g;
834                } else {
835                    userresults.push("–".to_string());
836                }
837            }
838
839            userresults[0] = format!("{}", summe);
840
841            let mut annotation = "".to_string();
842            let mut annotation_result = "".to_string();
843            if let Some(annotation_combined) = user.annotation {
844                let mut split = annotation_combined.splitn(2, '\x1f');
845
846                if let Some(ann) = split.next() {
847                    if ann.len() > 0 {
848                        annotation = ann.to_string();
849                        has_annotations = true;
850                    }
851                }
852
853                if let Some(ann) = split.next() {
854                    if ann.len() > 0 {
855                        annotation_result = ann.to_string();
856                        has_annotations_result = true;
857                    }
858                }
859            }
860
861            groupresults.push(UserResults { firstname: user.firstname.unwrap_or_else(|| "–".to_string()),
862                                            lastname: user.lastname.unwrap_or_else(|| "–".to_string()),
863                                            user_id: user.id,
864                                            grade: grade_to_string(user.grade),
865                                            logincode: user.logincode.unwrap_or_else(|| "".to_string()),
866                                            annotation,
867                                            annotation_result,
868                                            results: userresults });
869        }
870
871        results.push(GroupResults { groupname: group.name.to_string(),
872                                    group_id: group.id.unwrap_or(0),
873                                    groupcode: group.groupcode,
874                                    user_results: groupresults });
875    }
876
877    data.insert("taskname".to_string(), to_json(&tasknames));
878    data.insert("result".to_string(), to_json(&results));
879    data.insert("has_annotations".to_string(), to_json(&has_annotations));
880    data.insert("has_annotations_result".to_string(), to_json(&has_annotations_result));
881
882    let c = conn.get_contest_by_id(contest_id).ok_or(MedalError::UnknownId)?;
883    let ci = ContestInfo { id: c.id.unwrap(),
884                           name: c.name.clone(),
885                           duration: c.duration,
886                           public: c.public,
887                           requires_login: c.requires_login.unwrap_or(false),
888                           image: None,
889                           language: None,
890                           category: c.category.clone(),
891                           team_participation: false,
892                           tags: Vec::new() };
893
894    data.insert("contest".to_string(), to_json(&ci));
895    data.insert("contestname".to_string(), to_json(&c.name));
896
897    Ok(("contestresults".to_owned(), data))
898}
899
900pub fn start_contest<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str, csrf_token: &str,
901                                         secret: Option<String>, logincode_team: Option<String>)
902                                         -> MedalResult<Result<(), String>> {
903    // TODO: Is _or_new the right semantic? We need a CSRF token anyway …
904    let session = conn.get_session_or_new(&session_token).unwrap();
905    let contest = conn.get_contest_by_id(contest_id).ok_or(MedalError::UnknownId)?;
906
907    // Check logged in or open contest
908    if contest.duration != 0
909       && !session.is_logged_in()
910       && (contest.requires_login.unwrap_or(false) || contest.secret.is_none())
911    {
912        return Err(MedalError::AccessDenied);
913    }
914
915    // Check CSRF token
916    if session.is_logged_in() && session.csrf_token != csrf_token {
917        return Err(MedalError::CsrfCheckFailed);
918    }
919
920    // Check other constraints
921    let constraints = check_contest_constraints(&session, &contest);
922
923    if !(constraints.contest_running && constraints.grade_matching) {
924        return Err(MedalError::AccessDenied);
925    }
926
927    let is_qualified = check_contest_qualification(conn, &session, &contest);
928
929    if is_qualified == Some(false) {
930        return Err(MedalError::AccessDenied);
931    }
932
933    if contest.secret != secret {
934        return Err(MedalError::AccessDenied);
935    }
936
937    if let Some(logincode_team) = logincode_team {
938        match contest.max_teamsize {
939            None => return Err(MedalError::AccessDenied), // Contest does not allow team participation
940            Some(max_teamsize) => {
941                if max_teamsize < 2 {
942                    return Err(MedalError::AccessDenied); // Contest does not allow team participation
943                }
944            }
945        };
946
947        if logincode_team == "" {
948            return Ok(Err("no_logincode".to_string()));
949        }
950
951        let teampartner = conn.get_user_and_group_by_logincode(&logincode_team);
952        if let Some((teampartner_user, _)) = teampartner {
953            if teampartner_user.id == session.id {
954                return Ok(Err("own_logincode".to_string()));
955            }
956
957            if conn.get_participation(teampartner_user.id, contest_id).is_some() {
958                return Ok(Err("logincode_has_participation".to_string()));
959            }
960
961            if conn.get_participation(session.id, contest_id).is_some() {
962                return Err(MedalError::AccessDenied); // Contest already started TODO: Maybe redirect to page with hint
963            }
964
965            // Ok, we want a team participation and have a valid logincode and neither the user
966            // nor the team partner started the contest yet. We can now be relatively sure this
967            // will work out:
968            let res = conn.new_participation(session.id, contest_id, Some(session.id));
969            let _ = conn.new_participation(teampartner_user.id, contest_id, Some(session.id));
970            return match res {
971                Ok(_) => Ok(Ok(())),
972                _ => Err(MedalError::AccessDenied), // Contest already started TODO: Maybe redirect to page with hint
973            };
974        } else {
975            return Ok(Err("invalid_logincode".to_string()));
976        }
977    }
978
979    // Start contest
980    match conn.new_participation(session.id, contest_id, None) {
981        Ok(_) => Ok(Ok(())),
982        _ => Err(MedalError::AccessDenied), // Contest already started TODO: Maybe redirect to page with hint
983    }
984}
985
986pub fn login<T: MedalConnection>(conn: &T, login_data: (String, String), login_info: LoginInfo)
987                                 -> Result<String, MedalValue> {
988    let (username, password) = login_data;
989
990    match conn.login(None, &username, &password) {
991        Ok(session_token) => Ok(session_token),
992        Err(()) => {
993            let mut data = json_val::Map::new();
994            data.insert("reason".to_string(), to_json(&"Login fehlgeschlagen. Bitte erneut versuchen.".to_string()));
995            data.insert("username".to_string(), to_json(&username));
996            data.insert("parent".to_string(), to_json(&"base"));
997
998            fill_oauth_data(login_info, &mut data);
999
1000            Err(("login".to_owned(), data))
1001        }
1002    }
1003}
1004
1005pub fn login_with_code<T: MedalConnection>(
1006    conn: &T, code: &str, login_info: LoginInfo)
1007    -> Result<Result<String, String>, (String, json_val::Map<String, json_val::Value>)> {
1008    match conn.login_with_code(None, &code.trim()) {
1009        Ok(session_token) => Ok(Ok(session_token)),
1010        Err(()) => match conn.create_user_with_groupcode(None, &code.trim()) {
1011            Ok(session_token) => Ok(Err(session_token)),
1012            Err(()) => {
1013                let mut data = json_val::Map::new();
1014                data.insert("reason".to_string(), to_json(&"Kein gültiger Code. Bitte erneut versuchen.".to_string()));
1015                data.insert("code".to_string(), to_json(&code));
1016                data.insert("parent".to_string(), to_json(&"base"));
1017
1018                fill_oauth_data(login_info, &mut data);
1019
1020                Err(("login".to_owned(), data))
1021            }
1022        },
1023    }
1024}
1025
1026fn webauthn(self_url: &str) -> webauthn::Webauthn {
1027    use webauthn_rs::prelude::*;
1028
1029    // Get Domain name; make 'example.com' from 'https://example.com:8080/example/path'
1030    let rp_id = self_url.split("://").nth(1).unwrap().split('/').next().unwrap().split(':').next().unwrap();
1031    let rp_origin = Url::parse(self_url).expect("Invalid URL");
1032    WebauthnBuilder::new(rp_id, &rp_origin).expect("Invalid configuration").build().expect("Invalid configuration")
1033}
1034
1035pub fn register_key_challenge<T: MedalConnection>(conn: &T, self_url: &str, session_token: &str) -> JsonValueResult {
1036    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1037
1038    let webauthn = webauthn(self_url);
1039
1040    let cred_ids = conn.get_all_webauthn_credentials();
1041
1042    // Initiate a basic registration flow to enroll a cryptographic authenticator
1043    let (ccr, skr) =
1044        webauthn.start_passkey_registration(webauthn::Uuid::from_u64_pair(0, session.id as u64),
1045                                            session.firstname.as_ref().unwrap_or(&("".to_string())),
1046                                            session.firstname.as_ref().unwrap_or(&("".to_string())),
1047                                            Some(cred_ids.into_iter()
1048                                                         .map(|x| serde_json::from_str(&format!("\"{}\"", x)).unwrap())
1049                                                         .collect()))
1050                .expect("Failed to start webauthn registration.");
1051
1052    conn.set_webauthn_passkey_registration(session.id, &serde_json::json!(skr).to_string());
1053
1054    let mut data = json_val::Map::new();
1055    data.insert("challenge".to_string(), to_json(&ccr));
1056    Ok(data)
1057}
1058
1059pub fn register_key<T: MedalConnection>(conn: &T, self_url: &str, session_token: &str, csrf_token: &str,
1060                                        credential: String, name: String)
1061                                        -> JsonValueResult {
1062    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1063
1064    if session.csrf_token != csrf_token {
1065        return Err(MedalError::CsrfCheckFailed);
1066    }
1067
1068    let registration: String = conn.get_webauthn_passkey_registration(session.id).ok_or(MedalError::WebauthnError)?;
1069
1070    let webauthn = webauthn(self_url);
1071
1072    let registration: webauthn::PasskeyRegistration = serde_json::from_str(&registration).unwrap();
1073    let credential: webauthn::RegisterPublicKeyCredential = serde_json::from_str(&credential).unwrap();
1074
1075    let passkey = webauthn.finish_passkey_registration(&credential, &registration);
1076
1077    match passkey {
1078        Ok(passkey) => {
1079            if let serde_json::Value::String(cred_id) = serde_json::json!(passkey.cred_id()) {
1080                conn.add_webauthn_passkey(session.id, &cred_id, &serde_json::json!(passkey).to_string(), &name);
1081            } else {
1082                println!("Webauthn: Could not unwrap cred_id from webauthn Passkey");
1083                return Err(MedalError::WebauthnError);
1084            }
1085        }
1086        Err(webauthn::WebauthnError::UserNotVerified) => {
1087            // Happens if no pin is set e.g.
1088            println!("Webauthn: UserNotVerified");
1089            return Err(MedalError::WebauthnError);
1090        }
1091        Err(err) => {
1092            println!("Webauthn: Other error: {:#?}", err);
1093            return Err(MedalError::WebauthnError);
1094        }
1095    }
1096
1097    let data = json_val::Map::new();
1098    Ok(data)
1099}
1100
1101pub fn delete_key<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, token_id: i32)
1102                                      -> JsonValueResult {
1103    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1104
1105    if session.csrf_token != csrf_token {
1106        return Err(MedalError::CsrfCheckFailed);
1107    }
1108
1109    if !conn.get_webauthn_passkey_names_for_user(session.id).into_iter().any(|(id, _name)| id == token_id) {
1110        return Err(MedalError::NotFound);
1111    }
1112
1113    conn.delete_webauthn_passkey(session.id, token_id);
1114
1115    let data = json_val::Map::new();
1116    Ok(data)
1117}
1118
1119pub fn login_with_key_challenge<T: MedalConnection>(conn: &T, self_url: &str) -> JsonValue {
1120    let passkeys = conn.get_all_webauthn_passkeys()
1121                       .into_iter()
1122                       .map(|passkey| {
1123                           let passkey: webauthn::Passkey = serde_json::from_str(&passkey).unwrap();
1124                           passkey
1125                       })
1126                       .collect::<Vec<webauthn::Passkey>>();
1127
1128    let webauthn = webauthn(self_url);
1129
1130    let (challenge, authentication) = webauthn.start_passkey_authentication(&passkeys).unwrap();
1131
1132    let auth_id = conn.store_webauthn_auth_challenge(&serde_json::json!(challenge).to_string(),
1133                                                     &serde_json::json!(authentication).to_string());
1134
1135    let mut data = json_val::Map::new();
1136    data.insert("id".to_string(), to_json(&auth_id));
1137    data.insert("challenge".to_string(), to_json(&challenge));
1138    data
1139}
1140
1141pub fn login_with_key<T: MedalConnection>(conn: &T, self_url: &str, auth_id: i32, credential: &str)
1142                                          -> Result<String, String> {
1143    let authentication = conn.get_webauthn_auth_challenge_by_id(auth_id).unwrap();
1144
1145    let authentication: webauthn::PasskeyAuthentication = serde_json::from_str(&authentication).unwrap();
1146    let credential: webauthn::PublicKeyCredential = serde_json::from_str(&credential).unwrap();
1147
1148    let webauthn = webauthn(self_url);
1149
1150    match webauthn.finish_passkey_authentication(&credential, &authentication) {
1151        Err(err) => {
1152            println!("Webauthn: Other error: {:#?}", err);
1153            let mut data = json_val::Map::new();
1154            data.insert("reason".to_string(),
1155                        to_json(&"Unbekannter Key. Bitte Key zuerst im Profil registrieren.".to_string()));
1156            Err(serde_json::json!(data).to_string())
1157        }
1158        Ok(authresult) => {
1159            if let serde_json::Value::String(cred_id) = serde_json::json!(authresult.cred_id()) {
1160                match conn.login_with_key(None, &cred_id) {
1161                    Ok(session_token) => Ok(session_token),
1162                    Err(()) => {
1163                        let mut data = json_val::Map::new();
1164                        println!("Webauthn: Auth fail");
1165                        data.insert("reason".to_string(), to_json(&"Key konnte nicht authentifiziert werden. Bitte über anderen Weg einloggen.".to_string()));
1166                        Err(serde_json::json!(data).to_string())
1167                    }
1168                }
1169            } else {
1170                panic!("Webauthn: Could not unwrap cred_id from webauthn AuthenticationResult")
1171            }
1172        }
1173    }
1174}
1175
1176pub fn logout<T: MedalConnection>(conn: &T, session_token: Option<String>) {
1177    session_token.map(|token| conn.logout(&token));
1178}
1179
1180#[cfg(feature = "signup")]
1181pub fn signup<T: MedalConnection>(conn: &T, session_token: Option<String>, signup_data: (String, String, String))
1182                                  -> MedalResult<SignupResult> {
1183    let (username, email, password) = signup_data;
1184
1185    if username == "" || email == "" || password == "" {
1186        return Ok(SignupResult::EmptyFields);
1187    }
1188
1189    let salt = helpers::make_salt();
1190    let hash = helpers::hash_password(&password, &salt)?;
1191
1192    let result = conn.signup(&session_token.unwrap(), &username, &email, hash, &salt);
1193    Ok(result)
1194}
1195
1196#[cfg(feature = "signup")]
1197pub fn signupdata(query_string: Option<String>) -> json_val::Map<String, json_val::Value> {
1198    let mut data = json_val::Map::new();
1199    if let Some(query) = query_string {
1200        if let Some(status) = query.strip_prefix("status=") {
1201            if ["EmailTaken", "UsernameTaken", "UserLoggedIn", "EmptyFields"].contains(&status) {
1202                data.insert((status).to_string(), to_json(&true));
1203            }
1204        }
1205    }
1206    data
1207}
1208
1209pub fn load_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, subtask: Option<String>,
1210                                           submission_id: Option<i32>)
1211                                           -> MedalResult<String> {
1212    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1213
1214    match submission_id {
1215        None => match conn.load_submission(&session, task_id, subtask.as_deref()) {
1216            Some(submission) => Ok(submission.value),
1217            None => Ok("{}".to_string()),
1218        },
1219        Some(submission_id) => {
1220            let (submission, _, _, _) =
1221                conn.get_submission_by_id_complete_shallow_contest(submission_id).ok_or(MedalError::UnknownId)?;
1222
1223            // Is it not our own submission?
1224            if submission.user != session.id && !session.is_admin.unwrap_or(false) {
1225                if let Some((_, Some(group))) = conn.get_user_and_group_by_id(submission.user) {
1226                    if !group.admins.contains(&session.id) {
1227                        // We are not admin of the user's group
1228                        return Err(MedalError::AccessDenied);
1229                    }
1230                } else {
1231                    // The user has no group
1232                    return Err(MedalError::AccessDenied);
1233                }
1234            }
1235            Ok(submission.value)
1236        }
1237    }
1238}
1239
1240pub fn save_submission<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, csrf_token: &str,
1241                                           data: String, grade_percentage: i32, subtask: Option<String>)
1242                                           -> MedalResult<String> {
1243    let session = conn.get_session(&session_token).ensure_alive().ok_or(MedalError::NotLoggedIn)?;
1244
1245    if session.csrf_token != csrf_token {
1246        return Err(MedalError::CsrfCheckFailed);
1247    }
1248
1249    let (t, _, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1250
1251    match conn.get_participation(session.id, contest.id.expect("Value from database")) {
1252        None => return Err(MedalError::AccessDenied),
1253        Some(participation) => {
1254            let time_info = check_contest_time_left(&session, &contest, &participation);
1255            if !time_info.can_still_compete && time_info.left_secs_total < -10 {
1256                return Err(MedalError::AccessDenied);
1257                // Contest over
1258                // TODO: Nicer message!
1259            }
1260        }
1261    }
1262
1263    /* Here, two variants of the grade are calculated. Which one is correct depends on how the percentage value is
1264     * calculated in the task. Currently, grade_rounded is the correct one, but if that ever changes, the other code
1265     * can just be used.
1266     *
1267     * Switch to grade_truncated, when a user scores 98/99 but only gets 97/99 awarded.
1268     * Switch to grade_rounded, when a user scores 5/7 but only gets 4/7 awarded.
1269     */
1270
1271    /* Code for percentages calculated with integer rounding.
1272     *
1273     * This is a poor man's rounding that only works for division by 100.
1274     *
1275     *   floor((floor((x*10)/100)+5)/10) = round(x/100)
1276     */
1277    let grade_rounded = ((grade_percentage * t.stars * 10) / 100 + 5) / 10;
1278
1279    /* Code for percentages calculated with integer truncation.
1280     *
1281     * Why add one to grade_percentage and divide by 101?
1282     *
1283     * For all m in 1..100 and all n in 0..n, this holds:
1284     *
1285     *   floor( ((floor(n / m * 100)+1) * m ) / 101 ) = n
1286     *
1287     * Thus, when percentages are calculated as
1288     *
1289     *   p = floor(n / m * 100)
1290     *
1291     * we can recover n by using
1292     *
1293     *   n = floor( ((p+1) * m) / 101 )
1294     */
1295    // let grade_truncated = ((grade_percentage+1) * t.stars) / 101;
1296
1297    let submission = Submission { id: None,
1298                                  user: session.id,
1299                                  task: task_id,
1300                                  grade: grade_rounded,
1301                                  validated: false,
1302                                  nonvalidated_grade: grade_rounded,
1303                                  needs_validation: true,
1304                                  subtask_identifier: subtask,
1305                                  value: data,
1306                                  date: time::get_time() };
1307
1308    conn.submit_submission(submission);
1309
1310    Ok("{}".to_string())
1311}
1312
1313pub fn show_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, autosaveinterval: u64)
1314                                     -> MedalResult<Result<MedalValue, (i32, Option<String>)>> {
1315    let session = conn.get_session_or_new(&session_token).unwrap();
1316
1317    let (t, tg, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1318    let grade = conn.get_taskgroup_user_grade(session.id, tg.id.unwrap()); // TODO: Unwrap?
1319    let tasklist = conn.get_contest_by_id_complete(contest.id.unwrap()).ok_or(MedalError::UnknownId)?; // TODO: Unwrap?
1320
1321    let mut prevtaskgroup: Option<Taskgroup> = None;
1322    let mut nexttaskgroup: Option<Taskgroup> = None;
1323    let mut current_found = false;
1324
1325    let mut subtaskstars = Vec::new();
1326
1327    for taskgroup in tasklist.taskgroups {
1328        if current_found {
1329            nexttaskgroup = Some(taskgroup);
1330            break;
1331        }
1332
1333        if taskgroup.id == tg.id {
1334            current_found = true;
1335            subtaskstars = generate_subtaskstars(&taskgroup, &grade, Some(task_id));
1336        } else {
1337            prevtaskgroup = Some(taskgroup);
1338        }
1339    }
1340
1341    match conn.get_own_participation(session.id, contest.id.expect("Value from database")) {
1342        None => Ok(Err((contest.id.unwrap(), contest.category))),
1343        Some(participation) => {
1344            let mut data = json_val::Map::new();
1345            data.insert("subtasks".to_string(), to_json(&subtaskstars));
1346            data.insert("prevtask".to_string(), to_json(&prevtaskgroup.map(|tg| tg.tasks[0].id)));
1347            data.insert("nexttask".to_string(), to_json(&nexttaskgroup.map(|tg| tg.tasks[0].id))); // TODO: fail better
1348
1349            let time_info = check_contest_time_left(&session, &contest, &participation);
1350            data.insert("time_info".to_string(), to_json(&time_info));
1351
1352            data.insert("time_left_mh_formatted".to_string(),
1353                        to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1354            data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1355
1356            let auto_save_interval_ms = if autosaveinterval > 0 && autosaveinterval < 31536000000 {
1357                autosaveinterval * 1000
1358            } else {
1359                31536000000
1360            };
1361            data.insert("auto_save_interval_ms".to_string(), to_json(&auto_save_interval_ms));
1362
1363            if time_info.can_still_compete || time_info.is_review {
1364                data.insert("contestname".to_string(), to_json(&contest.name));
1365                data.insert("name".to_string(), to_json(&tg.name));
1366                data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name)));
1367                data.insert("taskid".to_string(), to_json(&task_id));
1368                data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1369                data.insert("contestid".to_string(), to_json(&contest.id));
1370                data.insert("readonly".to_string(), to_json(&time_info.is_review));
1371                data.insert("standalone_task".to_string(), to_json(&contest.standalone_task));
1372                data.insert("category".to_string(), to_json(&contest.category));
1373
1374                let (template, tasklocation) = if let Some(language) = t.language {
1375                    match language.as_str() {
1376                        "blockly" => ("wtask".to_owned(), t.location.as_str()),
1377                        "python" => {
1378                            data.insert("tasklang".to_string(), to_json(&"python"));
1379                            ("wtask".to_owned(), t.location.as_str())
1380                        }
1381                        _ => ("task".to_owned(), t.location.as_str()),
1382                    }
1383                } else {
1384                    match t.location.chars().next() {
1385                        Some('B') => ("wtask".to_owned(), &t.location[1..]),
1386                        Some('P') => {
1387                            data.insert("tasklang".to_string(), to_json(&"python"));
1388                            ("wtask".to_owned(), &t.location[1..])
1389                        }
1390                        _ => ("task".to_owned(), t.location.as_str()),
1391                    }
1392                };
1393
1394                let taskpath = format!("{}{}", contest.location, &tasklocation);
1395                data.insert("taskpath".to_string(), to_json(&taskpath));
1396
1397                Ok(Ok((template, data)))
1398            } else {
1399                // Contest over
1400                Ok(Err((contest.id.unwrap(), contest.category)))
1401            }
1402        }
1403    }
1404}
1405
1406pub fn review_task<T: MedalConnection>(conn: &T, task_id: i32, session_token: &str, submission_id: i32)
1407                                       -> MedalResult<Result<MedalValue, i32>> {
1408    let session = conn.get_session_or_new(&session_token).unwrap();
1409
1410    let (submission, t, tg, contest) =
1411        conn.get_submission_by_id_complete_shallow_contest(submission_id).ok_or(MedalError::UnknownId)?;
1412
1413    // TODO: We make a fake grade here, that represents this very submission, but maybe it is more sensible to retrieve
1414    // the actual grade here? If yes, use conn.get_taskgroup_user_grade(session.id, tg.id.unwrap());
1415    let grade = Grade { taskgroup: tg.id.unwrap(),
1416                        user: session.id,
1417                        grade: Some(submission.grade),
1418                        validated: submission.validated };
1419
1420    // Is it not our own submission?
1421    if submission.user != session.id && !session.is_admin.unwrap_or(false) {
1422        if let Some((_, Some(group))) = conn.get_user_and_group_by_id(submission.user) {
1423            if !group.admins.contains(&session.id) {
1424                // We are not admin of the user's group
1425                return Err(MedalError::AccessDenied);
1426            }
1427        } else {
1428            // The user has no group
1429            return Err(MedalError::AccessDenied);
1430        }
1431    }
1432
1433    let subtaskstars = generate_subtaskstars(&tg, &grade, Some(task_id)); // TODO does this work in general?
1434
1435    let mut data = json_val::Map::new();
1436    data.insert("subtasks".to_string(), to_json(&subtaskstars));
1437
1438    let time_info = ContestTimeInfo { passed_secs_total: 0,
1439                                      left_secs_total: 0,
1440                                      left_mins_total: 0,
1441                                      left_hour: 0,
1442                                      left_min: 0,
1443                                      left_sec: 0,
1444                                      has_timelimit: contest.duration != 0,
1445                                      is_time_left: false,
1446                                      exempt_from_timelimit: true,
1447                                      can_still_compete: false,
1448                                      review_has_timelimit: false,
1449                                      has_future_review: false,
1450                                      has_review_end: false,
1451                                      is_review: true,
1452                                      can_still_compete_or_review: true,
1453
1454                                      until_review_start_day: 0,
1455                                      until_review_start_hour: 0,
1456                                      until_review_start_min: 0,
1457
1458                                      until_review_end_day: 0,
1459                                      until_review_end_hour: 0,
1460                                      until_review_end_min: 0 };
1461
1462    data.insert("time_info".to_string(), to_json(&time_info));
1463
1464    data.insert("time_left_mh_formatted".to_string(),
1465                to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1466    data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1467
1468    data.insert("auto_save_interval_ms".to_string(), to_json(&0));
1469
1470    //data.insert("contestname".to_string(), to_json(&contest.name));
1471    data.insert("name".to_string(), to_json(&tg.name));
1472    data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name)));
1473    data.insert("taskid".to_string(), to_json(&task_id));
1474    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1475    //data.insert("contestid".to_string(), to_json(&contest.id));
1476    data.insert("readonly".to_string(), to_json(&time_info.is_review));
1477
1478    data.insert("submission".to_string(), to_json(&submission_id));
1479
1480    let (template, tasklocation) = if let Some(language) = t.language {
1481        match language.as_str() {
1482            "blockly" => ("wtask".to_owned(), t.location.as_str()),
1483            "python" => {
1484                data.insert("tasklang".to_string(), to_json(&"python"));
1485                ("wtask".to_owned(), t.location.as_str())
1486            }
1487            _ => ("task".to_owned(), t.location.as_str()),
1488        }
1489    } else {
1490        match t.location.chars().next() {
1491            Some('B') => ("wtask".to_owned(), &t.location[1..]),
1492            Some('P') => {
1493                data.insert("tasklang".to_string(), to_json(&"python"));
1494                ("wtask".to_owned(), &t.location[1..])
1495            }
1496            _ => ("task".to_owned(), t.location.as_str()),
1497        }
1498    };
1499
1500    let taskpath = format!("{}{}", contest.location, &tasklocation);
1501    data.insert("taskpath".to_string(), to_json(&taskpath));
1502
1503    Ok(Ok((template, data)))
1504}
1505
1506pub fn preview_task<T: MedalConnection>(conn: &T, task_id: i32) -> MedalResult<Result<MedalValue, i32>> {
1507    let (t, tg, contest) = conn.get_task_by_id_complete(task_id).ok_or(MedalError::UnknownId)?;
1508
1509    // Require a public, accessible standalone task
1510    if !contest.public
1511       || contest.duration != 0
1512       || contest.requires_contest.is_some()
1513       || contest.requires_login == Some(true)
1514       || contest.standalone_task != Some(true)
1515    {
1516        return Err(MedalError::UnknownId);
1517    }
1518
1519    let time_info = ContestTimeInfo { passed_secs_total: 0,
1520                                      left_secs_total: 0,
1521                                      left_mins_total: 0,
1522                                      left_hour: 0,
1523                                      left_min: 0,
1524                                      left_sec: 0,
1525                                      has_timelimit: contest.duration != 0,
1526                                      is_time_left: false,
1527                                      exempt_from_timelimit: true,
1528                                      can_still_compete: false,
1529                                      review_has_timelimit: false,
1530                                      has_future_review: false,
1531                                      has_review_end: false,
1532                                      is_review: true,
1533                                      can_still_compete_or_review: true,
1534
1535                                      until_review_start_day: 0,
1536                                      until_review_start_hour: 0,
1537                                      until_review_start_min: 0,
1538
1539                                      until_review_end_day: 0,
1540                                      until_review_end_hour: 0,
1541                                      until_review_end_min: 0 };
1542
1543    let mut data = json_val::Map::new();
1544
1545    data.insert("time_info".to_string(), to_json(&time_info));
1546
1547    data.insert("time_left_mh_formatted".to_string(),
1548                to_json(&format!("{}:{:02}", time_info.left_hour, time_info.left_min)));
1549    data.insert("time_left_sec_formatted".to_string(), to_json(&format!(":{:02}", time_info.left_sec)));
1550
1551    data.insert("auto_save_interval_ms".to_string(), to_json(&0));
1552
1553    data.insert("contestname".to_string(), to_json(&contest.name));
1554    data.insert("name".to_string(), to_json(&tg.name));
1555    data.insert("title".to_string(), to_json(&format!("Aufgabe „{}“ in {}", &tg.name, &contest.name)));
1556    data.insert("taskid".to_string(), to_json(&task_id));
1557    data.insert("contestid".to_string(), to_json(&contest.id));
1558    data.insert("readonly".to_string(), to_json(&time_info.is_review));
1559    data.insert("preview".to_string(), to_json(&true));
1560
1561    let (template, tasklocation) = if let Some(language) = t.language {
1562        match language.as_str() {
1563            "blockly" => ("wtask".to_owned(), t.location.as_str()),
1564            "python" => {
1565                data.insert("tasklang".to_string(), to_json(&"python"));
1566                ("wtask".to_owned(), t.location.as_str())
1567            }
1568            _ => ("task".to_owned(), t.location.as_str()),
1569        }
1570    } else {
1571        match t.location.chars().next() {
1572            Some('B') => ("wtask".to_owned(), &t.location[1..]),
1573            Some('P') => {
1574                data.insert("tasklang".to_string(), to_json(&"python"));
1575                ("wtask".to_owned(), &t.location[1..])
1576            }
1577            _ => ("task".to_owned(), t.location.as_str()),
1578        }
1579    };
1580
1581    let taskpath = format!("{}{}", contest.location, &tasklocation);
1582    data.insert("taskpath".to_string(), to_json(&taskpath));
1583
1584    Ok(Ok((template, data)))
1585}
1586
1587#[derive(Serialize, Deserialize)]
1588pub struct GroupInfo {
1589    pub id: i32,
1590    pub name: String,
1591    pub tag: String,
1592    pub code: String,
1593}
1594
1595pub fn show_groups<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
1596    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1597
1598    let mut data = json_val::Map::new();
1599    fill_user_data(&session, &mut data);
1600
1601    let v: Vec<GroupInfo> =
1602        conn.get_groups(session.id)
1603            .iter()
1604            .map(|g| GroupInfo { id: g.id.unwrap(),
1605                                 name: g.name.clone(),
1606                                 tag: g.tag.clone(),
1607                                 code: g.groupcode.clone() })
1608            .collect();
1609    data.insert("group".to_string(), to_json(&v));
1610    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1611
1612    Ok(("groups".to_string(), data))
1613}
1614
1615#[derive(Serialize, Deserialize)]
1616pub struct MemberInfo {
1617    pub id: i32,
1618    pub firstname: String,
1619    pub lastname: String,
1620    pub sex: String,
1621    pub grade: String,
1622    pub logincode: String,
1623    pub anonymous: bool,
1624}
1625
1626pub fn show_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
1627    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1628    let group = conn.get_group_complete(group_id).unwrap(); // TODO handle error
1629
1630    let mut data = json_val::Map::new();
1631    fill_user_data(&session, &mut data);
1632
1633    if !group.admins.contains(&session.id) {
1634        return Err(MedalError::AccessDenied);
1635    }
1636
1637    let gi = GroupInfo { id: group.id.unwrap(),
1638                         name: group.name.clone(),
1639                         tag: group.tag.clone(),
1640                         code: group.groupcode.clone() };
1641
1642    let v: Vec<MemberInfo> = group.members
1643                                  .iter()
1644                                  .filter_map(|m| {
1645                                      Some(MemberInfo { id: m.id,
1646                                                        firstname: m.firstname.clone()?,
1647                                                        lastname: m.lastname.clone()?,
1648                                                        sex: (match m.sex {
1649                                                                 Some(0) | None => "/",
1650                                                                 Some(1) => "m",
1651                                                                 Some(2) => "w",
1652                                                                 Some(3) => "d",
1653                                                                 Some(4) => "…",
1654                                                                 _ => "?",
1655                                                             }).to_string(),
1656                                                        grade: grade_to_string(m.grade),
1657                                                        logincode: m.logincode.clone()?,
1658                                                        anonymous: m.anonymous })
1659                                  })
1660                                  .collect();
1661
1662    data.insert("group".to_string(), to_json(&gi));
1663    data.insert("member".to_string(), to_json(&v));
1664    data.insert("groupname".to_string(), to_json(&gi.name));
1665
1666    Ok(("group".to_string(), data))
1667}
1668
1669pub fn add_group<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, name: String, tag: String)
1670                                     -> MedalResult<i32> {
1671    let session = conn.get_session(&session_token)
1672                      .ensure_logged_in()
1673                      .ok_or(MedalError::AccessDenied)?
1674                      .ensure_teacher_or_admin()
1675                      .ok_or(MedalError::AccessDenied)?;
1676
1677    if session.csrf_token != csrf_token {
1678        return Err(MedalError::CsrfCheckFailed);
1679    }
1680
1681    let mut groupcode = String::new();
1682    for i in 0..10 {
1683        if i == 9 {
1684            panic!("ERROR: Too many groupcode collisions! Give up ...");
1685        }
1686        groupcode = helpers::make_groupcode();
1687        if !conn.code_exists(&groupcode) {
1688            break;
1689        }
1690        println!("WARNING: Groupcode collision! Retrying ...");
1691    }
1692
1693    let mut group = Group { id: None, name, groupcode, tag, admins: vec![session.id], members: Vec::new() };
1694
1695    conn.add_group(&mut group);
1696
1697    Ok(group.id.unwrap())
1698}
1699
1700pub fn group_csv<T: MedalConnection>(conn: &T, session_token: &str, sex_infos: SexInformation) -> MedalValueResult {
1701    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1702
1703    let mut data = json_val::Map::new();
1704    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1705
1706    data.insert("require_sex".to_string(), to_json(&sex_infos.require_sex));
1707    data.insert("allow_sex_na".to_string(), to_json(&sex_infos.allow_sex_na));
1708    data.insert("allow_sex_diverse".to_string(), to_json(&sex_infos.allow_sex_diverse));
1709    data.insert("allow_sex_other".to_string(), to_json(&sex_infos.allow_sex_other));
1710
1711    Ok(("groupcsv".to_string(), data))
1712}
1713
1714// TODO: Should creating the users and groups happen in a batch operation to speed things up?
1715pub fn upload_groups<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, group_data: &str)
1716                                         -> MedalResult<()> {
1717    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1718
1719    if session.csrf_token != csrf_token {
1720        return Err(MedalError::CsrfCheckFailed);
1721    }
1722
1723    let mut v: Vec<Vec<String>> = serde_json::from_str(group_data).or(Err(MedalError::AccessDenied))?; // TODO: Change error type
1724    v.sort_unstable_by(|a, b| a[0].partial_cmp(&b[0]).unwrap());
1725
1726    let mut groupcode = String::new();
1727    let mut name = String::new();
1728    let mut group = Group { id: None,
1729                            name: name.clone(),
1730                            groupcode,
1731                            tag: String::new(),
1732                            admins: vec![session.id],
1733                            members: Vec::new() };
1734
1735    for line in v {
1736        if name != line[0] {
1737            if name != "" {
1738                conn.update_or_create_group_with_users(group, session.id);
1739            }
1740            name = line[0].clone();
1741
1742            groupcode = String::new();
1743            for i in 0..10 {
1744                if i == 9 {
1745                    panic!("ERROR: Too many groupcode collisions! Give up ...");
1746                }
1747                groupcode = helpers::make_groupcode();
1748                if !conn.code_exists(&groupcode) {
1749                    break;
1750                }
1751                println!("WARNING: Groupcode collision! Retrying ...");
1752            }
1753
1754            group = Group { id: None,
1755                            name: name.clone(),
1756                            groupcode,
1757                            tag: name.clone(),
1758                            admins: vec![session.id],
1759                            members: Vec::new() };
1760        }
1761
1762        let mut user = SessionUser::group_user_stub();
1763        user.grade = line[1].parse::<i32>().unwrap_or(0);
1764        user.firstname = Some(line[2].clone());
1765        user.lastname = Some(line[3].clone());
1766
1767        use db_objects::Sex;
1768        match line[4].as_str() {
1769            "m" => user.sex = Some(Sex::Male as i32),
1770            "f" => user.sex = Some(Sex::Female as i32),
1771            "d" => user.sex = Some(Sex::Diverse as i32),
1772            _ => user.sex = None,
1773        }
1774
1775        user.anonymous = line[5] == "y";
1776
1777        group.members.push(user);
1778    }
1779    conn.update_or_create_group_with_users(group, session.id);
1780
1781    Ok(())
1782}
1783
1784pub fn contest_result_csv<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
1785    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1786
1787    let mut data = json_val::Map::new();
1788    data.insert("csrf_token".to_string(), to_json(&session.csrf_token));
1789
1790    Ok(("admin_admissioncsv".to_string(), data))
1791}
1792
1793pub fn upload_contest_result_csv<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str,
1794                                                     contest_id: i32, admission_data: &str)
1795                                                     -> MedalResult<()> {
1796    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1797
1798    if session.csrf_token != csrf_token {
1799        return Err(MedalError::CsrfCheckFailed);
1800    }
1801
1802    let v: Vec<Vec<String>> = serde_json::from_str(admission_data).or(Err(MedalError::AccessDenied))?; // TODO: Change error type
1803
1804    let w: Vec<(i32, Option<String>)> = v.into_iter()
1805                                         .map(|vv| {
1806                                             (vv[0].parse().unwrap_or(-1),
1807                                              if vv[1].len() == 0 && vv[2].len() == 0 {
1808                                                  None
1809                                              } else {
1810                                                  // Using 0x1f (ACSII 31, unit seperator) as seperator
1811                                                  Some(format!("{}\x1f{}", vv[1], vv[2]))
1812                                              })
1813                                         })
1814                                         .collect();
1815
1816    let _annotations_inserted = conn.insert_contest_annotations(contest_id, w);
1817
1818    Ok(())
1819}
1820
1821#[allow(dead_code)]
1822pub fn show_groups_results<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalValueResult {
1823    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1824    //TODO: use g
1825    let _g = conn.get_contest_groups_grades(session.id, contest_id);
1826
1827    let data = json_val::Map::new();
1828
1829    Ok(("groupresults".into(), data))
1830}
1831
1832pub struct SexInformation {
1833    pub require_sex: bool,
1834    pub allow_sex_na: bool,
1835    pub allow_sex_diverse: bool,
1836    pub allow_sex_other: bool,
1837}
1838
1839pub fn show_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>,
1840                                        query_string: Option<String>, sex_infos: SexInformation)
1841                                        -> MedalValueResult {
1842    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
1843
1844    let mut data = json_val::Map::new();
1845    fill_user_data(&session, &mut data);
1846
1847    data.insert("require_sex".to_string(), to_json(&sex_infos.require_sex));
1848    data.insert("allow_sex_na".to_string(), to_json(&sex_infos.allow_sex_na));
1849    data.insert("allow_sex_diverse".to_string(), to_json(&sex_infos.allow_sex_diverse));
1850    data.insert("allow_sex_other".to_string(), to_json(&sex_infos.allow_sex_other));
1851
1852    match user_id {
1853        None => {
1854            data.insert("profile_firstname".to_string(), to_json(&session.firstname));
1855            data.insert("profile_lastname".to_string(), to_json(&session.lastname));
1856            data.insert("profile_street".to_string(), to_json(&session.street));
1857            data.insert("profile_zip".to_string(), to_json(&session.zip));
1858            data.insert("profile_city".to_string(), to_json(&session.city));
1859            data.insert(format!("sel{}", session.grade), to_json(&"selected"));
1860            if let Some(sex) = session.sex {
1861                data.insert(format!("sex_{}", sex), to_json(&"selected"));
1862            } else {
1863                data.insert("sex_None".to_string(), to_json(&"selected"));
1864            }
1865
1866            data.insert("profile_logincode".to_string(), to_json(&session.logincode));
1867            if session.password.is_some() {
1868                data.insert("profile_username".to_string(), to_json(&session.username));
1869            }
1870            if session.managed_by.is_none() {
1871                data.insert("profile_not_in_group".into(), to_json(&true));
1872            }
1873            if session.oauth_provider != Some("pms".to_string()) {
1874                data.insert("profile_not_pms".into(), to_json(&true));
1875                // This should be changed so that it can be configured if
1876                // addresses can be obtained from OAuth provider
1877            }
1878            data.insert("ownprofile".into(), to_json(&true));
1879            data.insert("userid".into(), to_json(&session.id));
1880
1881            let webauthn = conn.get_webauthn_passkey_names_for_user(session.id);
1882            data.insert("webauthn".into(), to_json(&webauthn));
1883            data.insert("has_webauthn".into(), to_json(&!webauthn.is_empty()));
1884
1885            if let Some(query) = query_string {
1886                if let Some(status) = query.strip_prefix("status=") {
1887                    if ["NothingChanged",
1888                        "DataChanged",
1889                        "PasswordChanged",
1890                        "PasswordMissmatch",
1891                        "firstlogin",
1892                        "SignedUp"].contains(&status)
1893                    {
1894                        data.insert((status).to_string(), to_json(&true));
1895                    }
1896                }
1897            }
1898
1899            let now = time::get_time();
1900
1901            // TODO: Needs to be filtered
1902            let participations: (Vec<(i32, String, bool, bool, bool)>, Vec<(i32, String, bool, bool, bool)>) =
1903                conn.get_all_participations_complete(session.id)
1904                    .into_iter()
1905                    .rev()
1906                    .map(|(participation, contest)| {
1907                        let passed_secs = now.sec - participation.start.sec;
1908                        let left_secs = i64::from(contest.duration) * 60 - passed_secs;
1909                        let is_time_left = contest.duration == 0 || left_secs >= 0;
1910                        let has_timelimit = contest.duration != 0;
1911                        let requires_login = contest.requires_login == Some(true);
1912                        (contest.id.unwrap(), contest.name, has_timelimit, is_time_left, requires_login)
1913                    })
1914                    .partition(|contest| contest.2 && !contest.4);
1915            data.insert("participations".into(), to_json(&participations));
1916
1917            let stars_count = conn.count_all_stars(session.id);
1918            data.insert("stars_count".into(), to_json(&stars_count));
1919            let stars_message = match stars_count {
1920                                    0 => "Auf gehts, dein erster Stern wartet auf dich!",
1921                                    1..=9 => "Ein hervorragender Anfang!",
1922                                    10..=99 => "Das ist ziemlich gut!",
1923                                    100..=999 => "Ein wahrer Meister!",
1924                                    _ => "Wow! Einfach wow!",
1925                                }.to_string();
1926
1927            data.insert("stars_message".into(), to_json(&stars_message));
1928        }
1929        // Case user_id: teacher modifing a students profile
1930        Some(user_id) => {
1931            // TODO: Add test to check if this access restriction works
1932            let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
1933            let group = opt_group.ok_or(MedalError::AccessDenied)?;
1934            if !group.admins.contains(&session.id) {
1935                return Err(MedalError::AccessDenied);
1936            }
1937
1938            data.insert("profile_firstname".to_string(), to_json(&user.firstname));
1939            data.insert("profile_lastname".to_string(), to_json(&user.lastname));
1940            data.insert("profile_street".to_string(), to_json(&session.street));
1941            data.insert("profile_zip".to_string(), to_json(&session.zip));
1942            data.insert("profile_city".to_string(), to_json(&session.city));
1943            data.insert(format!("sel{}", user.grade), to_json(&"selected"));
1944            if let Some(sex) = user.sex {
1945                data.insert(format!("sex_{}", sex), to_json(&"selected"));
1946            } else {
1947                data.insert("sex_None".to_string(), to_json(&"selected"));
1948            }
1949
1950            data.insert("profile_logincode".to_string(), to_json(&user.logincode));
1951            if user.username.is_some() {
1952                data.insert("profile_username".to_string(), to_json(&user.username));
1953            }
1954            if user.managed_by.is_none() {
1955                data.insert("profile_not_in_group".into(), to_json(&true));
1956            }
1957            if session.oauth_provider != Some("pms".to_string()) {
1958                data.insert("profile_not_pms".into(), to_json(&true));
1959            }
1960            data.insert("ownprofile".into(), to_json(&false));
1961
1962            if let Some(query) = query_string {
1963                if let Some(status) = query.strip_prefix("status=") {
1964                    if ["NothingChanged", "DataChanged", "PasswordChanged", "PasswordMissmatch"].contains(&status) {
1965                        data.insert((status).to_string(), to_json(&true));
1966                    }
1967                }
1968            }
1969        }
1970    }
1971
1972    Ok(("profile".to_string(), data))
1973}
1974
1975#[derive(Debug, PartialEq, Eq)]
1976pub enum ProfileStatus {
1977    NothingChanged,
1978    DataChanged,
1979    PasswordChanged,
1980    PasswordMissmatch,
1981}
1982impl From<ProfileStatus> for String {
1983    fn from(s: ProfileStatus) -> String { format!("{:?}", s) }
1984}
1985
1986pub fn edit_profile<T: MedalConnection>(conn: &T, session_token: &str, user_id: Option<i32>, csrf_token: &str,
1987                                        (firstname,
1988                                         lastname,
1989                                         street,
1990                                         zip,
1991                                         city,
1992                                         password,
1993                                         password_repeat,
1994                                         grade,
1995                                         sex): (String,
1996                                         String,
1997                                         Option<String>,
1998                                         Option<String>,
1999                                         Option<String>,
2000                                         Option<String>,
2001                                         Option<String>,
2002                                         i32,
2003                                         Option<i32>))
2004                                        -> MedalResult<ProfileStatus> {
2005    let mut session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2006
2007    if session.csrf_token != csrf_token {
2008        return Err(MedalError::AccessDenied); // CsrfError
2009    }
2010
2011    let mut result = ProfileStatus::NothingChanged;
2012
2013    let mut password_and_salt = None;
2014
2015    if let (Some(password), Some(password_repeat)) = (password, password_repeat) {
2016        if password != "" || password_repeat != "" {
2017            if password == password_repeat {
2018                let salt = helpers::make_salt();
2019                let hash = helpers::hash_password(&password, &salt)?;
2020
2021                password_and_salt = Some((hash, salt));
2022                result = ProfileStatus::PasswordChanged;
2023            } else {
2024                result = ProfileStatus::PasswordMissmatch;
2025            }
2026        }
2027    }
2028
2029    if result == ProfileStatus::NothingChanged {
2030        if session.firstname.as_ref() == Some(&firstname)
2031           && session.lastname.as_ref() == Some(&lastname)
2032           && session.street == street
2033           && session.zip == zip
2034           && session.city == city
2035           && session.grade == grade
2036           && session.sex == sex
2037        {
2038            return Ok(ProfileStatus::NothingChanged);
2039        } else {
2040            result = ProfileStatus::DataChanged;
2041        }
2042    }
2043
2044    match user_id {
2045        None => {
2046            session.firstname = Some(firstname);
2047            session.lastname = Some(lastname);
2048            session.grade = grade;
2049            session.sex = sex;
2050
2051            if street.is_some() {
2052                session.street = street;
2053            }
2054            if zip.is_some() {
2055                session.zip = zip;
2056            }
2057            if city.is_some() {
2058                session.city = city;
2059            }
2060
2061            if let Some((password, salt)) = password_and_salt {
2062                session.password = Some(password);
2063                session.salt = Some(salt);
2064            }
2065
2066            conn.save_session(session);
2067        }
2068        Some(user_id) => {
2069            // TODO: Add test to check if this access restriction works
2070            let (mut user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2071            let group = opt_group.ok_or(MedalError::AccessDenied)?;
2072            if !group.admins.contains(&session.id) {
2073                return Err(MedalError::AccessDenied);
2074            }
2075
2076            user.firstname = Some(firstname);
2077            user.lastname = Some(lastname);
2078            user.grade = grade;
2079            user.sex = sex;
2080
2081            if street.is_some() {
2082                user.street = street;
2083            }
2084            if zip.is_some() {
2085                user.zip = zip;
2086            }
2087            if city.is_some() {
2088                user.city = city;
2089            }
2090
2091            if let Some((password, salt)) = password_and_salt {
2092                user.password = Some(password);
2093                user.salt = Some(salt);
2094            }
2095
2096            conn.save_session(user);
2097        }
2098    }
2099
2100    Ok(result)
2101}
2102
2103pub fn check_profile<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str,
2104                                         (firstname, lastname): (String, String))
2105                                         -> JsonValueResult {
2106    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2107
2108    if session.csrf_token != csrf_token {
2109        return Err(MedalError::AccessDenied);
2110    }
2111
2112    let mut data = json_val::Map::new();
2113
2114    // Only activate for first login
2115    if session.firstname.map(|n| n.len() > 0).unwrap_or(false) && session.lastname.map(|n| n.len() > 0).unwrap_or(false)
2116    {
2117        data.insert("exists".to_string(), to_json(&false));
2118        return Ok(data);
2119    }
2120
2121    let group =
2122        conn.get_group_complete(session.managed_by.ok_or(MedalError::AccessDenied)?).ok_or(MedalError::AccessDenied)?;
2123
2124    for member in group.members {
2125        if member.lastname.map(|l| l.to_lowercase()) == Some(lastname.to_lowercase())
2126           && member.firstname.map(|l| l.to_lowercase()) == Some(firstname.to_lowercase())
2127           && member.id != session.id
2128        {
2129            data.insert("exists".to_string(), to_json(&true));
2130            return Ok(data);
2131        }
2132    }
2133
2134    data.insert("exists".to_string(), to_json(&false));
2135    Ok(data)
2136}
2137
2138pub fn teacher_infos<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2139    let session = conn.get_session(&session_token).ensure_logged_in().ok_or(MedalError::NotLoggedIn)?;
2140    if !session.is_teacher {
2141        return Err(MedalError::AccessDenied);
2142    }
2143
2144    let mut data = json_val::Map::new();
2145    fill_user_data(&session, &mut data);
2146
2147    Ok(("teacher".to_string(), data))
2148}
2149
2150pub fn admin_index<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2151    let session = conn.get_session(&session_token)
2152                      .ensure_logged_in()
2153                      .ok_or(MedalError::NotLoggedIn)?
2154                      .ensure_admin()
2155                      .ok_or(MedalError::AccessDenied)?;
2156
2157    let mut data = json_val::Map::new();
2158    fill_user_data(&session, &mut data);
2159
2160    Ok(("admin".to_string(), data))
2161}
2162
2163pub fn admin_search_users<T: MedalConnection>(conn: &T, session_token: &str,
2164                                              s_data: (Option<i32>,
2165                                               Option<String>,
2166                                               Option<String>,
2167                                               Option<String>,
2168                                               Option<String>,
2169                                               Option<String>))
2170                                              -> MedalValueResult {
2171    let session = conn.get_session(&session_token)
2172                      .ensure_logged_in()
2173                      .ok_or(MedalError::NotLoggedIn)?
2174                      .ensure_admin()
2175                      .ok_or(MedalError::AccessDenied)?;
2176
2177    let mut data = json_val::Map::new();
2178    fill_user_data(&session, &mut data);
2179
2180    match conn.get_search_users(s_data) {
2181        Ok(users) => {
2182            data.insert("users".to_string(), to_json(&users));
2183            data.insert("max_results".to_string(), to_json(&200));
2184            data.insert("num_results".to_string(), to_json(&users.len()));
2185            data.insert("no_results".to_string(), to_json(&(users.len() == 0)));
2186            if users.len() > 200 {
2187                data.insert("more_users".to_string(), to_json(&true));
2188                data.insert("more_results".to_string(), to_json(&true));
2189            }
2190        }
2191        Err(groups) => {
2192            data.insert("groups".to_string(), to_json(&groups));
2193            data.insert("max_results".to_string(), to_json(&200));
2194            data.insert("num_results".to_string(), to_json(&groups.len()));
2195            data.insert("no_results".to_string(), to_json(&(groups.len() == 0)));
2196            if groups.len() > 200 {
2197                data.insert("more_groups".to_string(), to_json(&true));
2198                data.insert("more_results".to_string(), to_json(&true));
2199            }
2200        }
2201    };
2202
2203    Ok(("admin_search_results".to_string(), data))
2204}
2205
2206pub fn admin_show_user<T: MedalConnection>(conn: &T, user_id: i32, session_token: &str) -> MedalValueResult {
2207    let session = conn.get_session(&session_token)
2208                      .ensure_logged_in()
2209                      .ok_or(MedalError::NotLoggedIn)?
2210                      .ensure_teacher_or_admin()
2211                      .ok_or(MedalError::AccessDenied)?;
2212
2213    let mut data = json_val::Map::new();
2214
2215    let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2216
2217    if !session.is_admin() {
2218        // Check access for teachers
2219        if let Some(group) = opt_group.clone() {
2220            if !group.admins.contains(&session.id) {
2221                return Err(MedalError::AccessDenied);
2222            }
2223        } else if user_id != session.id {
2224            return Err(MedalError::AccessDenied);
2225        }
2226    }
2227
2228    fill_user_data(&session, &mut data);
2229    fill_user_data_prefix(&user, &mut data, "user_");
2230    data.insert("user_logincode".to_string(), to_json(&user.logincode));
2231    data.insert("user_id".to_string(), to_json(&user.id));
2232    let grade = if user.grade >= 200 {
2233        "Kein Schüler mehr".to_string()
2234    } else if user.grade >= 11 {
2235        format!("{} ({})", user.grade % 100, if user.grade >= 100 { "G9" } else { "G8" })
2236    } else {
2237        format!("{}", user.grade)
2238    };
2239    data.insert("user_grade".to_string(), to_json(&grade));
2240    data.insert("user_oauthid".to_string(), to_json(&user.oauth_foreign_id));
2241    data.insert("user_oauthprovider".to_string(), to_json(&user.oauth_provider));
2242
2243    if let Some(group) = opt_group {
2244        data.insert("user_group_id".to_string(), to_json(&group.id));
2245        data.insert("user_group_name".to_string(), to_json(&group.name));
2246    }
2247
2248    let groups: Vec<GroupInfo> =
2249        conn.get_groups(user_id)
2250            .iter()
2251            .map(|g| GroupInfo { id: g.id.unwrap(),
2252                                 name: g.name.clone(),
2253                                 tag: g.tag.clone(),
2254                                 code: g.groupcode.clone() })
2255            .collect();
2256    data.insert("user_group".to_string(), to_json(&groups));
2257
2258    let parts = conn.get_all_participations_complete(user_id);
2259    let has_protected_participations = parts.iter().any(|p| p.1.protected);
2260    let contest_stars = conn.count_all_stars_by_contest(user_id);
2261
2262    let pi: Vec<(i32, String, String, i32)> =
2263        parts.into_iter()
2264             .map(|(p, c)| {
2265                 (c.id.unwrap(),
2266                  format!("{}{}{}",
2267                          &c.name,
2268                          if c.protected { " (geschützt)" } else { "" },
2269                          if p.team.is_some() { " (Teamteilnahme)" } else { "" }),
2270                  self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(p.start)).unwrap(),
2271                  contest_stars.iter()
2272                               .filter_map(|(id, stars)| if *id == c.id.unwrap() { Some(*stars) } else { None })
2273                               .next()
2274                               .unwrap_or(0))
2275             })
2276             .collect();
2277
2278    data.insert("user_participations".to_string(), to_json(&pi));
2279    data.insert("has_protected_participations".to_string(), to_json(&has_protected_participations));
2280    data.insert("can_delete".to_string(),
2281                to_json(&((!has_protected_participations || session.is_admin()) && groups.len() == 0)));
2282
2283    Ok(("admin_user".to_string(), data))
2284}
2285
2286pub fn admin_delete_user<T: MedalConnection>(conn: &T, user_id: i32, session_token: &str, csrf_token: &str)
2287                                             -> JsonValueResult {
2288    let session = conn.get_session(&session_token)
2289                      .ensure_logged_in()
2290                      .ok_or(MedalError::NotLoggedIn)?
2291                      .ensure_teacher_or_admin()
2292                      .ok_or(MedalError::AccessDenied)?;
2293
2294    if session.csrf_token != csrf_token {
2295        return Err(MedalError::CsrfCheckFailed);
2296    }
2297
2298    let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2299
2300    if !session.is_admin() {
2301        // Check access for teachers
2302        if let Some(group) = opt_group {
2303            if !group.admins.contains(&session.id) {
2304                return Err(MedalError::AccessDenied);
2305            }
2306        } else {
2307            return Err(MedalError::AccessDenied);
2308        }
2309    }
2310
2311    let parts = conn.get_all_participations_complete(user_id);
2312    let has_protected_participations = parts.iter().any(|p| p.1.protected);
2313    let groups = conn.get_groups(user_id);
2314
2315    let mut data = json_val::Map::new();
2316    if has_protected_participations && !session.is_admin() {
2317        data.insert("reason".to_string(), to_json(&"Benutzer hat Teilnahmen an geschützten Wettbewerben."));
2318        Err(MedalError::ErrorWithJson(data))
2319    } else if groups.len() > 0 {
2320        data.insert("reason".to_string(), to_json(&"Benutzer ist Administrator von Gruppen."));
2321        Err(MedalError::ErrorWithJson(data))
2322    } else {
2323        conn.delete_user(user_id);
2324        Ok(data)
2325    }
2326}
2327
2328pub fn admin_move_user_to_group<T: MedalConnection>(conn: &T, user_id: i32, group_id: i32, session_token: &str,
2329                                                    csrf_token: &str)
2330                                                    -> JsonValueResult {
2331    let session = conn.get_session(&session_token)
2332                      .ensure_logged_in()
2333                      .ok_or(MedalError::NotLoggedIn)?
2334                      .ensure_admin()
2335                      .ok_or(MedalError::AccessDenied)?;
2336
2337    if session.csrf_token != csrf_token {
2338        return Err(MedalError::CsrfCheckFailed);
2339    }
2340
2341    let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2342
2343    if !session.is_admin() {
2344        // Check access for teachers
2345        if let Some(group) = opt_group {
2346            if !group.admins.contains(&session.id) {
2347                return Err(MedalError::AccessDenied);
2348            }
2349        } else {
2350            return Err(MedalError::AccessDenied);
2351        }
2352    }
2353
2354    let mut data = json_val::Map::new();
2355    if conn.get_group(group_id).is_some() {
2356        if let Some(mut user) = conn.get_user_by_id(user_id) {
2357            user.managed_by = Some(group_id);
2358            conn.save_session(user);
2359            Ok(data)
2360        } else {
2361            data.insert("reason".to_string(), to_json(&"Benutzer existiert nicht."));
2362            Err(MedalError::ErrorWithJson(data))
2363        }
2364    } else {
2365        data.insert("reason".to_string(), to_json(&"Gruppe existiert nicht."));
2366        Err(MedalError::ErrorWithJson(data))
2367    }
2368}
2369
2370#[derive(Serialize, Deserialize)]
2371pub struct AdminInfo {
2372    pub id: i32,
2373    pub firstname: String,
2374    pub lastname: String,
2375}
2376
2377pub fn admin_show_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
2378    let session = conn.get_session(&session_token)
2379                      .ensure_logged_in()
2380                      .ok_or(MedalError::NotLoggedIn)?
2381                      .ensure_teacher_or_admin()
2382                      .ok_or(MedalError::AccessDenied)?;
2383
2384    let group = conn.get_group_complete(group_id).unwrap(); // TODO handle error
2385
2386    if !session.is_admin() {
2387        // Check access for teachers
2388        if !group.admins.contains(&session.id) {
2389            return Err(MedalError::AccessDenied);
2390        }
2391    }
2392
2393    let mut data = json_val::Map::new();
2394    fill_user_data(&session, &mut data);
2395
2396    let gi = GroupInfo { id: group.id.unwrap(),
2397                         name: group.name.clone(),
2398                         tag: group.tag.clone(),
2399                         code: group.groupcode.clone() };
2400
2401    let v: Vec<MemberInfo> =
2402        group.members
2403             .iter()
2404             .filter(|m| session.is_admin() || m.firstname.is_some() || m.lastname.is_some())
2405             .map(|m| MemberInfo { id: m.id,
2406                                   firstname: m.firstname.clone().unwrap_or_else(|| "".to_string()),
2407                                   lastname: m.lastname.clone().unwrap_or_else(|| "".to_string()),
2408                                   sex: (match m.sex {
2409                                            Some(0) | None => "/",
2410                                            Some(1) => "m",
2411                                            Some(2) => "w",
2412                                            Some(3) => "d",
2413                                            Some(4) => "…",
2414                                            _ => "?",
2415                                        }).to_string(),
2416                                   grade: grade_to_string(m.grade),
2417                                   logincode: m.logincode.clone().unwrap_or_else(|| "".to_string()),
2418                                   anonymous: m.anonymous })
2419             .collect();
2420
2421    let has_anonmous_members = group.members.iter().any(|m| m.anonymous);
2422
2423    let has_protected_participations = conn.group_has_protected_participations(group_id);
2424
2425    data.insert("group".to_string(), to_json(&gi));
2426    data.insert("member".to_string(), to_json(&v));
2427    data.insert("groupname".to_string(), to_json(&gi.name));
2428    data.insert("has_anonymous_members".to_string(), to_json(&has_anonmous_members));
2429    data.insert("has_protected_participations".to_string(), to_json(&has_protected_participations));
2430    data.insert("can_delete".to_string(), to_json(&(!has_protected_participations || session.is_admin())));
2431
2432    let admins: Vec<AdminInfo> =
2433        group.admins
2434             .iter()
2435             .map(|a| {
2436                 let admin = conn.get_user_by_id(*a).ok_or(MedalError::AccessDenied)?;
2437                 Ok(AdminInfo { id: admin.id,
2438                                firstname: admin.firstname.clone().unwrap_or_else(|| "".to_string()),
2439                                lastname: admin.lastname.clone().unwrap_or_else(|| "".to_string()) })
2440             })
2441             .collect::<Result<Vec<_>, _>>()?;
2442
2443    data.insert("group_admin".to_string(), to_json(&admins));
2444    data.insert("morethanoneadmin".to_string(), to_json(&(admins.len() > 1)));
2445
2446    Ok(("admin_group".to_string(), data))
2447}
2448
2449pub fn group_add_admin<T: MedalConnection>(conn: &T, group_id: i32, new_admin_id: i32, session_token: &str,
2450                                           csrf_token: &str)
2451                                           -> JsonValueResult {
2452    let session = conn.get_session(&session_token)
2453                      .ensure_logged_in()
2454                      .ok_or(MedalError::NotLoggedIn)?
2455                      .ensure_teacher_or_admin()
2456                      .ok_or(MedalError::AccessDenied)?;
2457
2458    if session.csrf_token != csrf_token {
2459        return Err(MedalError::CsrfCheckFailed);
2460    }
2461
2462    let mut group = conn.get_group(group_id).unwrap(); // TODO handle error
2463
2464    if !session.is_admin() {
2465        // If user is not a server admin, he must be a group admin
2466        if !group.admins.contains(&session.id) {
2467            return Err(MedalError::AccessDenied);
2468        }
2469    }
2470
2471    if group.admins.contains(&new_admin_id) {
2472        let mut data = json_val::Map::new();
2473        data.insert("reason".to_string(), to_json(&"Benutzer ist bereits Admin."));
2474        return Err(MedalError::ErrorWithJson(data));
2475    }
2476
2477    let new_admin = conn.get_user_by_id(new_admin_id).ok_or(MedalError::NotFound)?;
2478    let first_admin_id = group.admins.first().ok_or(MedalError::NotFound)?;
2479    let first_admin = conn.get_user_by_id(*first_admin_id).ok_or(MedalError::NotFound)?;
2480    if new_admin.oauth_provider == first_admin.oauth_provider {
2481        if let Some((_, new_admin_school)) = new_admin.oauth_foreign_id.ok_or(MedalError::AccessDenied)?.split_once('/')
2482        {
2483            if let Some((_, first_admin_school)) =
2484                first_admin.oauth_foreign_id.ok_or(MedalError::AccessDenied)?.split_once('/')
2485            {
2486                if new_admin_school == first_admin_school && new_admin_school.len() >= 1 {
2487                    conn.add_admin_to_group(&mut group, new_admin_id);
2488
2489                    let data = json_val::Map::new();
2490                    return Ok(data);
2491                }
2492            }
2493        }
2494    }
2495
2496    let mut data = json_val::Map::new();
2497    data.insert("reason".to_string(),
2498                to_json(&"Benutzer gehört nicht zur gleichen Schule oder Benutzer nicht als Lehrkraft angemeldet."));
2499    Err(MedalError::ErrorWithJson(data))
2500}
2501
2502pub fn admin_delete_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str, csrf_token: &str)
2503                                              -> JsonValueResult {
2504    let session = conn.get_session(&session_token)
2505                      .ensure_logged_in()
2506                      .ok_or(MedalError::NotLoggedIn)?
2507                      .ensure_teacher_or_admin()
2508                      .ok_or(MedalError::AccessDenied)?;
2509
2510    if session.csrf_token != csrf_token {
2511        return Err(MedalError::CsrfCheckFailed);
2512    }
2513
2514    let group = conn.get_group(group_id).unwrap(); // TODO handle error
2515
2516    if !session.is_admin() {
2517        // Check access for teachers
2518        if !group.admins.contains(&session.id) {
2519            return Err(MedalError::AccessDenied);
2520        }
2521    }
2522
2523    let mut data = json_val::Map::new();
2524    if conn.group_has_protected_participations(group_id) && !session.is_admin() {
2525        data.insert("reason".to_string(), to_json(&"Gruppe hat Mitglieder mit geschützten Teilnahmen."));
2526        Err(MedalError::ErrorWithJson(data))
2527    } else {
2528        conn.delete_all_users_for_group(group_id);
2529        conn.delete_group(group_id);
2530        Ok(data)
2531    }
2532}
2533
2534pub fn admin_show_edit_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str) -> MedalValueResult {
2535    let session = conn.get_session(&session_token)
2536                      .ensure_logged_in()
2537                      .ok_or(MedalError::NotLoggedIn)?
2538                      .ensure_teacher_or_admin()
2539                      .ok_or(MedalError::AccessDenied)?;
2540
2541    let group = conn.get_group_complete(group_id).unwrap(); // TODO handle error
2542
2543    if !session.is_admin() {
2544        // Check access for teachers
2545        if !group.admins.contains(&session.id) {
2546            return Err(MedalError::AccessDenied);
2547        }
2548    }
2549
2550    let mut data = json_val::Map::new();
2551    fill_user_data(&session, &mut data);
2552
2553    let gi = GroupInfo { id: group.id.unwrap(),
2554                         name: group.name.clone(),
2555                         tag: group.tag.clone(),
2556                         code: group.groupcode.clone() };
2557
2558    data.insert("group".to_string(), to_json(&gi));
2559    data.insert("groupname".to_string(), to_json(&gi.name));
2560    data.insert("grouptag".to_string(), to_json(&gi.name));
2561
2562    Ok(("admin_edit_group".to_string(), data))
2563}
2564
2565pub fn admin_edit_group<T: MedalConnection>(conn: &T, group_id: i32, session_token: &str, csrf_token: &str,
2566                                            name: String, tag: String)
2567                                            -> MedalResult<()> {
2568    let session = conn.get_session(&session_token)
2569                      .ensure_logged_in()
2570                      .ok_or(MedalError::NotLoggedIn)?
2571                      .ensure_teacher_or_admin()
2572                      .ok_or(MedalError::AccessDenied)?;
2573
2574    if session.csrf_token != csrf_token {
2575        return Err(MedalError::CsrfCheckFailed);
2576    }
2577
2578    let mut group = conn.get_group_complete(group_id).unwrap(); // TODO handle error
2579
2580    if !session.is_admin() {
2581        // Check access for teachers
2582        if !group.admins.contains(&session.id) {
2583            return Err(MedalError::AccessDenied);
2584        }
2585    }
2586
2587    group.name = name;
2588    group.tag = tag;
2589
2590    conn.save_group(&mut group);
2591
2592    Ok(())
2593}
2594
2595#[derive(Serialize, Deserialize, Debug)]
2596struct SubmissionResult {
2597    id: i32,
2598    grade: i32,
2599    date: String,
2600}
2601#[derive(Serialize, Deserialize, Debug)]
2602struct TaskResult {
2603    id: i32,
2604    stars: i32,
2605    submissions: Vec<SubmissionResult>,
2606}
2607#[derive(Serialize, Deserialize, Debug)]
2608struct TaskgroupResult {
2609    id: i32,
2610    name: String,
2611    tasks: Vec<TaskResult>,
2612}
2613
2614pub fn admin_show_participation<T: MedalConnection>(conn: &T, user_id: i32, contest_id: i32, session_token: &str)
2615                                                    -> MedalValueResult {
2616    let session = conn.get_session(&session_token)
2617                      .ensure_logged_in()
2618                      .ok_or(MedalError::NotLoggedIn)?
2619                      .ensure_teacher_or_admin()
2620                      .ok_or(MedalError::AccessDenied)?;
2621
2622    let user = conn.get_user_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2623    let participation = conn.get_participation(user.id, contest_id).ok_or(MedalError::AccessDenied)?;
2624
2625    let user_or_team = if let Some(team) = participation.team { team } else { user_id };
2626
2627    let (_, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2628
2629    if !session.is_admin() {
2630        // Check access for teachers
2631        if let Some(ref group) = opt_group {
2632            if !group.admins.contains(&session.id) {
2633                return Err(MedalError::AccessDenied);
2634            }
2635        } else {
2636            return Err(MedalError::AccessDenied);
2637        }
2638    }
2639
2640    let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
2641    let grades = conn.get_contest_user_grades(user_or_team, contest_id);
2642
2643    let mut totalgrade = 0;
2644    let mut max_totalgrade = 0;
2645
2646    for taskgroup in &contest.taskgroups {
2647        max_totalgrade += taskgroup.tasks.iter().map(|x| x.stars).max().unwrap_or(0);
2648    }
2649    for grade in grades {
2650        totalgrade += grade.grade.unwrap_or(0);
2651    }
2652
2653    #[rustfmt::skip]
2654    let subms: Vec<TaskgroupResult> =
2655        contest.taskgroups
2656            .into_iter()
2657            .map(|tg| TaskgroupResult {
2658                id: tg.id.unwrap(),
2659                name: tg.name,
2660                tasks: tg.tasks
2661                    .into_iter()
2662                    .map(|t| TaskResult {
2663                        id: t.id.unwrap(),
2664                        stars: t.stars,
2665                        submissions: conn.get_all_submissions(user_or_team, t.id.unwrap(), None)
2666                            .into_iter()
2667                            .map(|s| SubmissionResult {
2668                                id: s.id.unwrap(),
2669                                grade: s.grade,
2670                                date: self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(s.date)).unwrap(),
2671                            })
2672                            .collect(),
2673                      })
2674                      .collect(),
2675               })
2676               .collect();
2677
2678    let mut data = json_val::Map::new();
2679
2680    data.insert("submissions".to_string(), to_json(&subms));
2681    data.insert("contestid".to_string(), to_json(&contest.id));
2682    data.insert("contestname".to_string(), to_json(&contest.name));
2683    data.insert("has_timelimit".to_string(), to_json(&(contest.duration > 0)));
2684
2685    data.insert("total_grade".to_string(), to_json(&totalgrade));
2686    data.insert("max_total_grade".to_string(), to_json(&max_totalgrade));
2687
2688    if let Some(group) = opt_group {
2689        data.insert("group_id".to_string(), to_json(&group.id));
2690        data.insert("group_name".to_string(), to_json(&group.name));
2691    }
2692
2693    fill_user_data(&session, &mut data);
2694    fill_user_data_prefix(&user, &mut data, "user_");
2695    data.insert("user_id".to_string(), to_json(&user.id));
2696
2697    data.insert("start_date".to_string(),
2698                to_json(&self::time::strftime("%e. %b %Y, %H:%M", &self::time::at(participation.start)).unwrap()));
2699
2700    data.insert("can_delete".to_string(), to_json(&(!contest.protected || session.is_admin.unwrap_or(false))));
2701    Ok(("admin_participation".to_string(), data))
2702}
2703
2704pub fn admin_delete_participation<T: MedalConnection>(conn: &T, user_id: i32, contest_id: i32, session_token: &str,
2705                                                      csrf_token: &str)
2706                                                      -> JsonValueResult {
2707    let session = conn.get_session(&session_token)
2708                      .ensure_logged_in()
2709                      .ok_or(MedalError::NotLoggedIn)?
2710                      .ensure_teacher_or_admin()
2711                      .ok_or(MedalError::AccessDenied)?;
2712
2713    if session.csrf_token != csrf_token {
2714        return Err(MedalError::CsrfCheckFailed);
2715    }
2716
2717    let (user, opt_group) = conn.get_user_and_group_by_id(user_id).ok_or(MedalError::AccessDenied)?;
2718    let _part = conn.get_participation(user.id, contest_id).ok_or(MedalError::AccessDenied)?;
2719    let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
2720
2721    if !session.is_admin() {
2722        // Check access for teachers
2723        if contest.protected {
2724            return Err(MedalError::AccessDenied);
2725        }
2726
2727        if let Some(group) = opt_group {
2728            if !group.admins.contains(&session.id) {
2729                return Err(MedalError::AccessDenied);
2730            }
2731        } else {
2732            return Err(MedalError::AccessDenied);
2733        }
2734    }
2735
2736    let mut data = json_val::Map::new();
2737    fill_user_data(&session, &mut data);
2738
2739    conn.delete_participation(user_id, contest_id);
2740    Ok(data)
2741}
2742
2743pub fn admin_show_contests<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2744    let session = conn.get_session(&session_token)
2745                      .ensure_logged_in()
2746                      .ok_or(MedalError::NotLoggedIn)?
2747                      .ensure_admin()
2748                      .ok_or(MedalError::AccessDenied)?;
2749
2750    let mut data = json_val::Map::new();
2751    fill_user_data(&session, &mut data);
2752
2753    let mut contests: Vec<_> = conn.get_contest_list().into_iter().map(|contest| (contest.id, contest.name)).collect();
2754    contests.sort(); // Sort by id (sorts by natural order on fields)
2755    contests.reverse();
2756
2757    data.insert("contests".to_string(), to_json(&contests));
2758
2759    Ok(("admin_contests".to_string(), data))
2760}
2761
2762pub fn admin_contest_export<T: MedalConnection>(conn: &T, contest_id: i32, session_token: &str) -> MedalResult<String> {
2763    conn.get_session(&session_token)
2764        .ensure_logged_in()
2765        .ok_or(MedalError::NotLoggedIn)?
2766        .ensure_admin()
2767        .ok_or(MedalError::AccessDenied)?;
2768
2769    let contest = conn.get_contest_by_id_complete(contest_id).ok_or(MedalError::UnknownId)?;
2770
2771    let taskgroup_ids: Vec<(i32, String)> =
2772        contest.taskgroups.into_iter().map(|tg| (tg.id.unwrap(), tg.name)).collect();
2773    let filename = format!("contest_{}__{}__{}.csv",
2774                           contest_id,
2775                           self::time::strftime("%F_%H-%M-%S", &self::time::now()).unwrap(),
2776                           helpers::make_filename_secret());
2777
2778    conn.export_contest_results_to_file(contest_id, &taskgroup_ids, &format!("./export/{}", filename));
2779
2780    Ok(filename)
2781}
2782
2783pub fn admin_show_cleanup<T: MedalConnection>(conn: &T, session_token: &str) -> MedalValueResult {
2784    let session = conn.get_session(&session_token)
2785                      .ensure_logged_in()
2786                      .ok_or(MedalError::NotLoggedIn)?
2787                      .ensure_admin()
2788                      .ok_or(MedalError::AccessDenied)?;
2789
2790    let mut data = json_val::Map::new();
2791    fill_user_data(&session, &mut data);
2792
2793    let now = time::get_time();
2794    let maxage = now - time::Duration::days(30); // Count all temporary sessions with 30 days of inactivity
2795    let n_temporary_session = conn.count_temporary_sessions(maxage);
2796    data.insert("temporary_session_count".to_string(), to_json(&n_temporary_session));
2797
2798    Ok(("admin_cleanup".to_string(), data))
2799}
2800
2801pub fn admin_do_cleanup<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str) -> JsonValueResult {
2802    let session = conn.get_session(&session_token)
2803                      .ensure_logged_in()
2804                      .ok_or(MedalError::NotLoggedIn)?
2805                      .ensure_admin()
2806                      .ok_or(MedalError::AccessDenied)?;
2807
2808    if session.csrf_token != csrf_token {
2809        return Err(MedalError::CsrfCheckFailed);
2810    }
2811
2812    let now = time::get_time();
2813    let maxstudentage = now - time::Duration::days(180); // Delete managed users after 180 days of inactivity
2814    let maxteacherage = now - time::Duration::days(1095); // Delete teachers after 3 years of inactivity
2815    let maxage = now - time::Duration::days(3650); // Delete every user after 10 years of inactivity
2816
2817    let result = conn.remove_old_users_and_groups(maxstudentage, Some(maxteacherage), Some(maxage));
2818
2819    let mut data = json_val::Map::new();
2820    if let Ok((n_user, n_group, n_teacher, n_other)) = result {
2821        data.insert("n_user".to_string(), to_json(&n_user));
2822        data.insert("n_group".to_string(), to_json(&n_group));
2823        data.insert("n_teacher".to_string(), to_json(&n_teacher));
2824        data.insert("n_other".to_string(), to_json(&n_other));
2825        Ok(data)
2826    } else {
2827        data.insert("reason".to_string(), to_json(&"Datenbank-Fehler."));
2828        Err(MedalError::ErrorWithJson(data))
2829    }
2830}
2831
2832pub fn do_session_cleanup<T: MedalConnection>(conn: &T) -> JsonValueResult {
2833    let now = time::get_time();
2834    let maxage = now - time::Duration::days(30); // Delete all temporary sessions after 30 days of inactivity
2835
2836    conn.remove_temporary_sessions(maxage, Some(1000));
2837
2838    let data = json_val::Map::new();
2839    Ok(data)
2840}
2841
2842pub fn move_task_location<T: MedalConnection>(conn: &T, session_token: &str, csrf_token: &str, old_location: &str,
2843                                              new_location: &str, contest: Option<i32>)
2844                                              -> JsonValueResult {
2845    let session = conn.get_session(&session_token)
2846                      .ensure_logged_in()
2847                      .ok_or(MedalError::NotLoggedIn)?
2848                      .ensure_admin()
2849                      .ok_or(MedalError::AccessDenied)?;
2850
2851    if session.csrf_token != csrf_token {
2852        return Err(MedalError::CsrfCheckFailed);
2853    }
2854
2855    let mut data = json_val::Map::new();
2856    if old_location == new_location {
2857        data.insert("reason".to_string(), to_json(&"old and new location identical"));
2858        return Err(MedalError::ErrorWithJson(data));
2859    }
2860
2861    let n_contest = conn.move_task_location(old_location, new_location, contest);
2862
2863    data.insert("contests_modified".to_string(), to_json(&n_contest));
2864
2865    Ok(data)
2866}
2867
2868#[derive(PartialEq, Eq, Clone, Copy)]
2869pub enum UserType {
2870    User,
2871    Teacher,
2872    Admin,
2873}
2874
2875pub enum UserSex {
2876    Female,
2877    Male,
2878    Unknown,
2879}
2880
2881pub struct ForeignUserData {
2882    pub foreign_id: String,
2883    pub foreign_type: UserType,
2884    pub sex: UserSex,
2885    pub firstname: String,
2886    pub lastname: String,
2887    pub school_name: Option<String>,
2888}
2889
2890pub fn login_oauth<T: MedalConnection>(conn: &T, user_data: ForeignUserData, oauth_provider_id: String)
2891                                       -> Result<(String, bool), (String, json_val::Map<String, json_val::Value>)> {
2892    match conn.login_foreign(None,
2893                             &oauth_provider_id,
2894                             &user_data.foreign_id,
2895                             (user_data.foreign_type != UserType::User,
2896                              user_data.foreign_type == UserType::Admin,
2897                              &user_data.firstname,
2898                              &user_data.lastname,
2899                              match user_data.sex {
2900                                  UserSex::Male => Some(1),
2901                                  UserSex::Female => Some(2),
2902                                  UserSex::Unknown => Some(0),
2903                              },
2904                              &user_data.school_name))
2905    {
2906        Ok((session_token, last_activity)) => {
2907            let redirect_profile = if let Some(last_activity) = last_activity {
2908                let now = time::get_time();
2909                now - last_activity > time::Duration::days(60)
2910            } else {
2911                true
2912            };
2913
2914            // While we're at it, lets also remove some old sessions! OAuth took
2915            // some time anyway, so we can afford spending some additional
2916            // milliseconds …
2917            let now = time::get_time();
2918            let maxage = now - time::Duration::days(30); // Delete all temporary sessions after 30 days of inactivity
2919            conn.remove_temporary_sessions(maxage, Some(200));
2920
2921            Ok((session_token, redirect_profile))
2922        }
2923        Err(()) => {
2924            let mut data = json_val::Map::new();
2925            data.insert("reason".to_string(), to_json(&"OAuth-Login failed.".to_string()));
2926            Err(("login".to_owned(), data))
2927        }
2928    }
2929}