1use std::path::{Path, PathBuf};
16
17use structopt::StructOpt;
18
19#[derive(Serialize, Deserialize, Clone, Default, Debug)]
20pub struct OauthProvider {
21 pub provider_id: String,
22 pub medal_oauth_type: String,
23 pub url: String,
24 pub client_id: String,
25 pub client_secret: String,
26 pub access_token_url: String,
27 pub user_data_url: String,
28 pub school_data_url: Option<String>,
29 pub school_data_secret: Option<String>,
30 pub allow_teacher_login_without_school: Option<bool>,
31 pub login_link_text: String,
32}
33
34#[derive(Serialize, Deserialize, Clone, Default, Debug)]
35pub struct Config {
36 pub host: Option<String>,
37 pub port: Option<u16>,
38 pub self_url: Option<String>,
39 pub oauth_providers: Option<Vec<OauthProvider>>,
40 pub database_file: Option<PathBuf>,
41 pub database_url: Option<String>,
42 pub template: Option<String>,
43 pub no_contest_scan: Option<bool>,
44 pub open_browser: Option<bool>,
45 pub cookie_signing_secret: Option<String>,
46 pub disable_results_page: Option<bool>,
47 pub enable_password_login: Option<bool>,
48 pub require_sex: Option<bool>,
49 pub allow_sex_na: Option<bool>,
50 pub allow_sex_diverse: Option<bool>,
51 pub allow_sex_other: Option<bool>,
52 pub dbstatus_secret: Option<String>,
53 pub template_params: Option<::std::collections::BTreeMap<String, serde_json::Value>>,
54 pub only_contest_scan: Option<bool>,
55 pub reset_admin_pw: Option<bool>,
56 pub log_timing: Option<bool>,
57 pub auto_save_interval: Option<u64>,
58 pub version: Option<bool>,
59 pub restricted_task_directories: Option<Vec<String>>,
60 pub autoclean_submissions: Option<bool>,
61}
62
63#[derive(StructOpt, Debug)]
64#[structopt()]
65struct Opt {
66 #[structopt(short = "c", long = "config", default_value = "config.yaml", parse(from_os_str))]
68 pub configfile: PathBuf,
69
70 #[structopt(short = "d", long = "database", parse(from_os_str))]
72 pub databasefile: Option<PathBuf>,
73
74 #[structopt(short = "D", long = "databaseurl")]
76 pub databaseurl: Option<String>,
77
78 #[structopt(short = "p", long = "port")]
80 pub port: Option<u16>,
81
82 #[structopt(short = "t", long = "template")]
84 pub template: Option<String>,
85
86 #[structopt(short = "a", long = "reset-admin-pw")]
88 pub resetadminpw: bool,
89
90 #[structopt(short = "S", long = "no-contest-scan")]
92 pub nocontestscan: bool,
93
94 #[structopt(short = "s", long = "only-contest-scan")]
96 pub onlycontestscan: bool,
97
98 #[structopt(short = "b", long = "browser")]
100 pub openbrowser: bool,
101
102 #[structopt(long = "disable-results-page")]
104 pub disableresultspage: bool,
105
106 #[structopt(short = "P", long = "passwordlogin")]
108 pub enablepasswordlogin: bool,
109
110 #[structopt(short = "T", long = "teacherpage")]
112 pub teacherpage: Option<String>,
113
114 #[structopt(long = "log-timing")]
116 pub logtiming: bool,
117
118 #[structopt(long = "auto-save-interval")]
120 pub autosaveinterval: Option<u64>,
121
122 #[structopt(long = "version")]
124 pub version: bool,
125}
126
127enum FileType {
128 Json,
129 Yaml,
130}
131
132pub fn read_config_from_file(file: &Path) -> Config {
133 use std::io::Read;
134
135 let file_type = match file.extension().map(|e| e.to_str().unwrap_or("<Encoding error>")) {
136 Some("yaml") | Some("YAML") => FileType::Yaml,
137 Some("json") | Some("JSON") => FileType::Json,
138 Some(ext) => panic!("Config file has unknown file extension `{}` (supported types are YAML and JSON).", ext),
139 None => panic!("Config file has no file extension (supported types are YAML and JSON)."),
140 };
141
142 println!("Reading configuration file '{}'", file.to_str().unwrap_or("<Encoding error>"));
143
144 let mut config: Config = if let Ok(mut file) = std::fs::File::open(file) {
145 let mut contents = String::new();
146 file.read_to_string(&mut contents).unwrap();
147 match file_type {
148 FileType::Json => serde_json::from_str(&contents).unwrap(),
149 FileType::Yaml => serde_yaml::from_str(&contents).unwrap(),
150 }
151 } else {
152 println!("Configuration file '{}' not found. Using default configuration.",
153 file.to_str().unwrap_or("<Encoding error>"));
154 Default::default()
155 };
156
157 if let Some(ref rtds) = config.restricted_task_directories {
158 for rtd in rtds {
160 if !Path::new(rtd).exists() {
161 println!("WARNING: restricted task directory '{}' does NOT exist!", rtd);
162 }
163 if rtd.chars().last().unwrap_or(' ') != '/' {
164 println!("WARNING: restricted task directory '{}' does NOT end with a '/'", rtd);
165 }
166 if !rtd.starts_with("tasks/") {
167 println!("WARNING: restricted task directory '{}' does NOT start with 'tasks/'", rtd);
168 }
169 if rtd == "tasks/" {
170 println!("WARNING: restricted task directory '{}' restricts ALL tasks", rtd);
171 }
172 }
173 }
174
175 if let Some(ref oap) = config.oauth_providers {
176 println!("OAuth providers:");
177 for oap in oap {
178 println!(" * {}", oap.provider_id);
179 }
180 }
181
182 if config.host.is_none() {
183 config.host = Some("[::]".to_string())
184 }
185 if config.port.is_none() {
186 config.port = Some(8080)
187 }
188 if config.self_url.is_none() {
189 config.self_url = Some("http://localhost:8080".to_string())
190 }
191 if config.template.is_none() {
192 config.template = Some("default".to_string())
193 }
194 if config.no_contest_scan.is_none() {
195 config.no_contest_scan = Some(false)
196 }
197 if config.open_browser.is_none() {
198 config.open_browser = Some(false)
199 }
200 if config.enable_password_login.is_none() {
201 config.enable_password_login = Some(false)
202 }
203 if config.auto_save_interval.is_none() {
204 config.auto_save_interval = Some(10)
205 }
206
207 println!("OAuth providers will be told to redirect to {}", config.self_url.as_ref().unwrap());
208
209 config
210}
211
212fn merge_value<T>(into: &mut Option<T>, from: Option<T>) { from.map(|x| *into = Some(x)); }
213
214fn merge_flag(into: &mut Option<bool>, from: bool) {
215 if from {
216 *into = Some(true);
217 }
218}
219
220pub fn get_config() -> Config {
221 let opt = Opt::from_args();
222 if opt.version {
223 return Config { version: Some(true), ..Config::default() };
224 }
225
226 #[cfg(feature = "debug")]
227 println!("Options: {:#?}", opt);
228
229 let mut config = read_config_from_file(&opt.configfile);
230
231 #[cfg(feature = "debug")]
232 println!("Config: {:#?}", config);
233
234 merge_value(&mut config.database_file, opt.databasefile);
236 merge_value(&mut config.database_url, opt.databaseurl);
237 merge_value(&mut config.port, opt.port);
238 merge_value(&mut config.template, opt.template);
239 merge_value(&mut config.auto_save_interval, opt.autosaveinterval);
240
241 merge_flag(&mut config.no_contest_scan, opt.nocontestscan);
242 merge_flag(&mut config.open_browser, opt.openbrowser);
243 merge_flag(&mut config.disable_results_page, opt.disableresultspage);
244 merge_flag(&mut config.enable_password_login, opt.enablepasswordlogin);
245 merge_flag(&mut config.only_contest_scan, opt.onlycontestscan);
246 merge_flag(&mut config.reset_admin_pw, opt.resetadminpw);
247 merge_flag(&mut config.log_timing, opt.logtiming);
248
249 if let Some(template_params) = &mut config.template_params {
250 if let Some(teacherpage) = opt.teacherpage {
251 template_params.insert("teacher_page".to_string(), teacherpage.into());
252 }
253
254 let all_categories = if let Some(serde_json::Value::Object(categories)) = template_params.get("categories") {
255 Some(categories.clone())
256 } else {
257 None
258 };
259
260 if let Some(serde_json::Value::Array(tiles)) = template_params.get_mut("index_tiles") {
261 for (i, elem) in tiles.iter_mut().enumerate() {
262 if let serde_json::Value::Object(tile) = elem {
263 if i == 0 {
264 tile.insert("medal_index_tile_is_first".to_string(), serde_json::Value::Bool(true));
265 }
266 if i % 2 == 0 {
267 tile.insert("medal_index_tile_is_even".to_string(), serde_json::Value::Bool(true));
268 }
269 if let Some(serde_json::Value::String(category_name)) = tile.get("category") {
270 if let Some(Some(serde_json::Value::Object(category))) =
271 all_categories.as_ref().map(|c| c.get(category_name))
272 {
273 for (key, value) in category {
274 tile.insert(key.clone(), value.clone());
275 }
276 }
277 }
278 }
279 }
280 }
281 } else if let Some(teacherpage) = opt.teacherpage {
282 let mut template_params = ::std::collections::BTreeMap::<String, serde_json::Value>::new();
283 template_params.insert("teacher_page".to_string(), teacherpage.into());
284 config.template_params = Some(template_params);
285 }
286
287 config.database_file.get_or_insert(Path::new("medal.db").to_owned());
289
290 config
291}