extern crate getopts; extern crate pencil; extern crate modelthing; extern crate env_logger; extern crate pulldown_cmark; extern crate regex; #[macro_use] extern crate lazy_static; #[macro_use] extern crate log; use std::env; use std::fs::{File, create_dir}; use std::io::Write; use std::collections::BTreeMap; use std::path::Path; use getopts::Options; use pencil::Pencil; use pencil::{Request, PencilResult, Response, HTTPError, PencilError}; use pencil::{redirect, abort}; use pencil::method::{Get, Post}; use regex::Regex; use modelthing::transpile_scheme::TranspileScheme; use modelthing::transpile_js::{TranspileJS, TranspileJSODE}; use modelthing::repr_latex::{ReprLaTeX, ReprVarsHTML}; /* This command doesn't use error_chain (or raise errors in general) because the web framework handles panics as 500 errors automatically. */ fn home(r: &mut Request) -> PencilResult { let context: BTreeMap = BTreeMap::new(); return r.app.render_template("home.html", &context); } fn readme(r: &mut Request) -> PencilResult { let mut context = BTreeMap::new(); let raw_text = include_str!("../../README.txt"); context.insert("raw_text".to_string(), raw_text.to_string()); return r.app.render_template("raw.html", &context); } fn model_list(r: &mut Request) -> PencilResult { let namespace = r.view_args.get("namespace").unwrap(); if !is_clean_name(namespace) { return abort(404); } if !(namespace == "examples" || namespace == "sandbox") { // TODO: check if namespace path exists, else 404 return abort(404); } let paths = modelthing::search_models(Path::new(namespace)); let l: Vec> = paths.iter() .map(|x| vec![namespace.to_string(), Path::new(x).strip_prefix(namespace) .unwrap() .to_string_lossy() .to_string() ]) .collect(); let mut context = BTreeMap::new(); context.insert("model_slug_list".to_string(), l); context.insert("namespace".to_string(), vec![vec![namespace.to_string()]]); return r.app.render_template("model_list.html", &context); } fn model_view(r: &mut Request) -> PencilResult { let namespace = r.view_args.get("namespace").unwrap(); let model_slug = r.view_args.get("model_slug").unwrap(); let model_path = Path::new(namespace).join(model_slug); match modelthing::load_model_entry(model_path.as_path()) { Ok(me) => { let mut markdown_html = String::new(); let p = pulldown_cmark::Parser::new_ext( &me.markdown, pulldown_cmark::Options::empty()); pulldown_cmark::html::push_html(&mut markdown_html, p); let mut context = BTreeMap::new(); context.insert("namespace".to_string(), namespace.to_string()); context.insert("model_slug".to_string(), model_slug.to_string()); context.insert("model_name".to_string(), me.ast.name.clone()); context.insert("model_description".to_string(), me.ast.description.clone().unwrap_or("".to_string())); context.insert("markdown_html".to_string(), markdown_html); context.insert("markdown".to_string(), me.markdown.clone()); context.insert("modelica".to_string(), format!("{:?}", me.ast)); context.insert("latex".to_string(), me.ast.repr_latex().unwrap()); context.insert("vars_table".to_string(), me.ast.repr_vars_html().unwrap()); context.insert("editable".to_string(), if namespace == "sandbox" { "true".to_string() } else { "".to_string() }); context.insert("computable".to_string(), if me.ast.transpile_scheme().is_ok() { "true".to_string() } else { "".to_string() }); r.app.render_template("model_view.html", &context) }, Err(_) => Err(PencilError::PenHTTPError(pencil::HTTPError::NotFound)), } } fn model_repr(r: &mut Request) -> PencilResult { let namespace = r.view_args.get("namespace").unwrap(); let model_slug = r.view_args.get("model_slug").unwrap(); let model_path = Path::new(namespace).join(model_slug); let format = r.url .query_pairs() .filter(|x| x.0 == "format") .map(|(_,v)| v.into_owned()) .nth(0) .unwrap_or("".to_string()); let format: &str = &format; match modelthing::load_model_entry(model_path.as_path()) { Ok(me) => { match format { "scheme" => Ok(Response::from(me.ast.transpile_scheme().unwrap())), "javascript" => Ok(Response::from(me.ast.transpile_js().unwrap())), "javascript_ode" => Ok(Response::from(me.ast.transpile_js_ode().unwrap())), "latex" => Ok(Response::from(me.ast.repr_latex().unwrap())), _ => abort(403), } }, Err(_) => Err(PencilError::PenHTTPError(pencil::HTTPError::NotFound)), } } // Sends the modelica file fn model_raw(r: &mut Request) -> PencilResult { let namespace = r.view_args.get("namespace").unwrap(); let model_slug = r.view_args.get("model_slug").unwrap(); let model_path = Path::new(namespace).join(model_slug).to_string_lossy().to_string(); pencil::helpers::send_from_directory(&model_path, "model.modelica", true) } // Sends the markdown fn model_markdown(r: &mut Request) -> PencilResult { let namespace = r.view_args.get("namespace").unwrap(); let model_slug = r.view_args.get("model_slug").unwrap(); let model_path = Path::new(namespace).join(model_slug).to_string_lossy().to_string(); pencil::helpers::send_from_directory(&model_path, "page.md", true) } fn model_edit(r: &mut Request) -> PencilResult { let namespace = r.view_args.get("namespace").unwrap().clone(); let model_slug = r.view_args.get("model_slug").unwrap().clone(); let model_path = Path::new(&namespace).join(&model_slug); let mut context = BTreeMap::new(); if namespace != "sandbox" { warn!("Tried to write outside of sandbox: {}", namespace); return abort(403); } match modelthing::load_model_entry(model_path.as_path()) { Ok(me) => { context.insert("namespace".to_string(), namespace.to_string()); context.insert("model_slug".to_string(), model_slug.to_string()); context.insert("model_name".to_string(), me.ast.name.clone()); context.insert("markdown".to_string(), me.markdown.clone()); context.insert("modelica".to_string(), format!("{:?}", me.ast)); match r.method { Get => { r.app.render_template("model_edit.html", &context) }, Post => { let modelica = r.form().get("modelica").expect("missing form val: modelica").clone(); let markdown = r.form().get("markdown").expect("missing form val: markdown").clone(); // Check that modelica is valid if let Err(e) = modelthing::modelica_parser::parse_model(&modelica) { error!("Modelica code not valid: {:?}", e); return abort(500); }; { // overwrite existing let mut f = File::create(format!("{}/{}/page.md", namespace, model_slug)) .expect("unable to create wiki file"); f.write_all(markdown.as_bytes()).expect("unable to write wiki file"); } { // overwrite existing let mut f = File::create( format!("{}/{}/model.modelica", namespace, model_slug)) .expect("unable to create modelicafile"); f.write_all(modelica.as_bytes()).expect("unable to write modelica file"); } redirect(&format!("/m/{}/{}/", namespace, model_slug), 301) }, _ => { warn!("Invalid method attempted"); abort(404) }, } }, Err(_) => Err(PencilError::PenHTTPError(pencil::HTTPError::NotFound)), } } fn is_clean_name(s: &str) -> bool { lazy_static! { static ref RE: Regex = Regex::new(r"^[0-9A-Za-z_-]+$").unwrap(); } RE.is_match(s) } #[test] fn test_is_clean_name() { assert_eq!(is_clean_name("name"), true); assert_eq!(is_clean_name("name.two"), false); assert_eq!(is_clean_name("name/thing"), false); assert_eq!(is_clean_name("unicode_α"), false); assert_eq!(is_clean_name("name_thing"), true); assert_eq!(is_clean_name("name-thing"), true); assert_eq!(is_clean_name("1234"), true); } fn model_create(r: &mut Request) -> PencilResult { let context: BTreeMap = BTreeMap::new(); match r.method { Get => { r.app.render_template("model_create.html", &context) }, Post => { let namespace = r.form().get("namespace").expect("missing form val: namespace").clone(); let model_slug = r.form().get("model_slug").expect("missing form val: model_slug").clone(); if namespace != "sandbox" { warn!("Tried to write outside of sandbox: {}", namespace); return abort(403); } if !(is_clean_name(&namespace) && is_clean_name(&model_slug)) { warn!("Unclean: {} or {}", namespace, model_slug); return abort(404); } let modelica = &format!( "model {}\n\n\"blank description, edit me\"\n\tReal a;\nequation\n\ta = 1;\nend {};\n", model_slug, model_slug); let wiki_text = "blank wikitext; edit me!"; // Check that modelica is valid if let Err(e) = modelthing::modelica_parser::parse_model(modelica) { error!("Modelica code not valid: {:?}", e); return abort(500); }; // Ensure that namespace directory exists if let Err(e) = create_dir(format!("{}", namespace)) { error!("Couldn't create namespace diectory: {:?}", e); return abort(500); }; if let Err(e) = create_dir(format!("{}/{}", namespace, model_slug)) { error!("Couldn't create model diectory: {:?}", e); return abort(500); }; { let mut f = File::create(format!("{}/{}/page.md", namespace, model_slug)) .expect("unable to create wiki file"); f.write_all(wiki_text.as_bytes()).expect("unable to write wiki file"); } { let mut f = File::create( format!("{}/{}/model.modelica", namespace, model_slug)) .expect("unable to create modelicafile"); f.write_all(modelica.as_bytes()).expect("unable to write modelica file"); } redirect(&format!("/m/{}/{}/", namespace, model_slug), 301) }, _ => { warn!("Invalid method attempted"); abort(404) }, } } fn page_not_found(_: HTTPError) -> PencilResult { let mut response = Response::from("404: Not Found"); response.status_code = 404; Ok(response) } fn server_error(_: HTTPError) -> PencilResult { let mut response = Response::from("500: Server Error"); response.status_code = 500; Ok(response) } fn print_usage(opts: Options) { let brief = "usage:\tmt-webface [options]"; println!(""); print!("{}", opts.usage(&brief)); } fn main() { let args: Vec = env::args().collect(); let mut opts = Options::new(); opts.optflag("h", "help", "print this help menu"); opts.optflag("b", "bind", "local IP:port to bind to"); opts.optflag("", "version", "print the version"); let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => { println!("{}\n", f.to_string()); print_usage(opts); ::std::process::exit(-1); } }; if matches.opt_present("help") { print_usage(opts); return; } if matches.opt_present("version") { println!("modelthing {}", env!("CARGO_PKG_VERSION")); return; } env_logger::init().unwrap(); let mut app = Pencil::new("webface"); app.name = "modelthing-webface".to_string(); app.set_debug(true); app.set_log_level(); app.httperrorhandler(404, page_not_found); app.httperrorhandler(500, server_error); app.enable_static_file_handling(); debug!("root_path: {}", app.root_path); app.register_template("base.html"); app.register_template("home.html"); app.register_template("raw.html"); app.get("/", "home", home); app.get("/readme/", "readme", readme); app.register_template("model_list.html"); app.get("/m//", "model_list", model_list); app.register_template("model_view.html"); app.get("/m///", "model_view", model_view); app.get("/m///raw/", "model_raw", model_raw); app.get("/m///repr/", "model_repr", model_repr); app.get("/m///markdown/", "model_markdown", model_markdown); app.register_template("model_create.html"); app.route("/create/", &[Get, Post], "model_create", model_create); app.register_template("model_edit.html"); app.route("/m///edit/", &[Get, Post], "model_edit", model_edit); let bind = matches.opt_str("bind").unwrap_or("127.0.0.1:5000".to_string()); let bind_str: &str = &bind; info!("Running on {}", bind); app.run(bind_str); }