use chrono::NaiveDateTime; use git2::Commit; use git2::FileMode; use git2::ObjectType; use git2::Oid; use git2::Repository; use git2::Tree; use git2::TreeEntry; use hyper::header; use hyper::http::request::Parts; use serde::Deserialize; use sputnik::html_escape; use sputnik::request::SputnikParts; use std::str::from_utf8; use crate::action_links; use crate::controller::Controller; use crate::diff::diff; use crate::forms; use crate::get_renderer; use crate::ActionParam; use crate::Context; use crate::Error; use crate::HyperResponse; use crate::Page; use crate::RenderMode; use crate::Response; pub(crate) fn get_blob( entr: TreeEntry, params: ActionParam, controller: &C, ctx: Context, parts: &Parts, ) -> Result { match params.action.as_ref() { "view" => view_blob(entr, params, controller, ctx, parts), "edit" => edit_blob(entr, params, controller, ctx, parts), "upload" => Ok(forms::upload_form(true, controller, &ctx, parts).into()), "log" => log_blob(entr, params, controller, ctx, parts), "diff" => diff_blob(entr, params, controller, ctx, parts), "raw" => raw_blob(entr, params, controller, ctx, parts).map(|r| r.into()), "run" => run_blob(entr, params, controller, ctx, parts).map(|r| r.into()), "move" => move_blob(entr, params, controller, ctx, parts), "remove" => remove_blob(params, controller, ctx, parts), _ => Err(Error::BadRequest("unknown action".into())), } } fn view_blob( entr: TreeEntry, params: ActionParam, controller: &C, ctx: Context, parts: &Parts, ) -> Result { let mut page = Page { title: ctx.path.file_name().unwrap().to_owned(), header: action_links(¶ms.action, controller, &ctx, parts), ..Default::default() }; if entr.filemode() == FileMode::Link.into() { // TODO: indicate and link symbolic link } let blob = ctx.repo.find_blob(entr.id()).unwrap(); if let Some(mime) = mime_guess::from_path(&ctx.path).first() { if mime.type_() == "image" { page.body .push_str("
"); return Ok(page.into()); } } match from_utf8(blob.content()) { Ok(text) => { if let Some(renderer) = get_renderer(&ctx.path) { renderer(text, &mut page, RenderMode::View); } else { page.body .push_str(&format!("
{}
", html_escape(text))); } } Err(_) => page.body.push_str("failed to decode file as UTF-8"), } Ok(page.into()) } fn edit_blob( entr: TreeEntry, _params: ActionParam, controller: &C, ctx: Context, parts: &Parts, ) -> Result { if !controller.may_write_path(&ctx, parts) { return Err(Error::Unauthorized( "you are not authorized to edit this file".into(), )); } let blob = ctx.repo.find_blob(entr.id()).unwrap(); if let Ok(text) = from_utf8(blob.content()) { Ok(forms::edit_text_form( &forms::EditForm { text: text.to_string(), oid: Some(entr.id().to_string()), ..Default::default() }, None, controller, &ctx, parts, ) .into()) } else { Ok(forms::upload_form(true, controller, &ctx, parts).into()) } } fn log_blob( _entr: TreeEntry, params: ActionParam, controller: &C, ctx: Context, parts: &Parts, ) -> Result { let filename = ctx.path.file_name().unwrap(); let mut page = Page { title: format!("Log for {}", filename), header: action_links(¶ms.action, controller, &ctx, parts), ..Default::default() }; let mut walk = ctx.repo.revwalk()?; let branch_head = ctx.branch_head()?; walk.push(branch_head.id())?; let mut prev_commit = branch_head; let mut prev_blobid = Some(prev_commit.tree()?.get_path(ctx.path.as_ref())?.id()); let mut commits = Vec::new(); // TODO: paginate for oid in walk.flatten().skip(1) { let commit = ctx.repo.find_commit(oid)?; if let Ok(entr) = commit.tree()?.get_path(ctx.path.as_ref()) { let blobid = entr.id(); if Some(blobid) != prev_blobid { commits.push(prev_commit); prev_blobid = Some(blobid); } prev_commit = commit; } else { if prev_blobid.is_some() { commits.push(prev_commit); } prev_commit = commit; prev_blobid = None; } } if prev_commit.parent_count() == 0 && prev_commit.tree()?.get_path(ctx.path.as_ref()).is_ok() { // the very first commit of the branch commits.push(prev_commit); } page.body.push_str( "
\ ", ); let mut prev_date = None; for c in commits { let date = NaiveDateTime::from_timestamp(c.time().seconds(), 0).date(); if Some(date) != prev_date { if prev_date != None { page.body.push_str(""); } page.body.push_str(&format!( "

{}

    ", date.format("%b %d, %Y") )); } page.body.push_str(&format!( "
  • \ \ ", c.id().to_string() )); page.body.push_str(&format!( "{}: {}
  • ", html_escape(c.id().to_string()), html_escape(c.author().name().unwrap_or_default()), html_escape(c.summary().unwrap_or_default()), )); prev_date = Some(date); } page.body.push_str("
"); Ok(page.into()) } #[derive(Deserialize)] struct DiffParams { id: String, oldid: Option, } fn find_commit<'a>(repo: &'a Repository, id: &str, branch_head: &Oid) -> Option> { let oid = Oid::from_str(id).ok()?; repo.find_commit(oid) .ok() // disallow viewing commits from other branches you shouldn't have access to .filter(|c| oid == *branch_head || repo.graph_descendant_of(*branch_head, c.id()).unwrap()) } fn diff_blob( _entr: TreeEntry, action_param: ActionParam, controller: &C, ctx: Context, parts: &Parts, ) -> Result { let params: DiffParams = parts.query()?; let branch_commit = ctx.branch_head()?; let mut commit = find_commit(&ctx.repo, ¶ms.id, &branch_commit.id()) .ok_or_else(|| Error::NotFound("commit not found".into()))?; let old_commit = if let Some(oldid) = ¶ms.oldid { let other_commit = find_commit(&ctx.repo, oldid, &branch_commit.id()) .ok_or_else(|| Error::NotFound("commit not found".into()))?; if other_commit.time() > commit.time() { // TODO: redirect instead let c = commit; commit = other_commit; Some(c) } else { Some(other_commit) } } else { // TODO: what if there are multiple parents? commit.parents().next() }; let blob_id = if let Ok(entry) = commit.tree()?.get_path(ctx.path.as_ref()) { entry.id() } else { return Ok(Page { title: format!("Commit for {}", ctx.path.file_name().unwrap()), body: "file removed".into(), header: action_links(&action_param.action, controller, &ctx, parts), ..Default::default() } .into()); }; let old_blob_id = old_commit .and_then(|p| p.tree().unwrap().get_path(ctx.path.as_ref()).ok()) .map(|e| e.id()); if Some(blob_id) == old_blob_id { return Err(Error::NotFound("no difference".into())); } let mut page = Page { title: format!( "{} for {}", if params.oldid.is_some() { "Diff" } else { "Commit" }, ctx.path.file_name().unwrap() ), header: action_links(&action_param.action, controller, &ctx, parts), ..Default::default() }; page.body.push_str("
"); if params.oldid.is_none() { page.body.push_str(&format!( "

{}

{} committed on {}", html_escape(commit.summary().unwrap_or_default()), html_escape(commit.author().name().unwrap_or_default()), NaiveDateTime::from_timestamp(commit.time().seconds(), 0).format("%b %d, %Y, %H:%M") )); } // TODO: if UTF-8 decoding fails, link ?action=raw&rev= if let Some(old_blob_id) = old_blob_id { page.body.push_str(&diff( from_utf8(ctx.repo.find_blob(old_blob_id)?.content())?, from_utf8(ctx.repo.find_blob(blob_id)?.content())?, )) } else { page.body.push_str(&diff( "", from_utf8(ctx.repo.find_blob(blob_id)?.content())?, )); } page.body.push_str("
"); Ok(page.into()) } fn build_raw_response( entr: TreeEntry, ctx: &Context, parts: &Parts, ) -> Result { if let Some(etag) = parts .headers .get(header::IF_NONE_MATCH) .and_then(|v| v.to_str().ok()) { if etag.trim_start_matches("W/").trim_matches('"') == entr.id().to_string() { return Err(Error::NotModified); } } let blob = ctx.repo.find_blob(entr.id()).unwrap(); let mut resp = HyperResponse::new(blob.content().to_owned().into()); resp.headers_mut() .insert(header::ETAG, format!("\"{}\"", entr.id()).parse().unwrap()); resp.headers_mut() .insert(header::CACHE_CONTROL, "no-cache".parse().unwrap()); Ok(resp) } fn raw_blob( entr: TreeEntry, _params: ActionParam, _controller: &C, ctx: Context, parts: &Parts, ) -> Result { let mut resp = build_raw_response(entr, &ctx, parts)?; if let Some(mime) = mime_guess::from_path(&ctx.path).first() { if mime.type_() == "text" { // workaround for Firefox, which downloads non-plain text subtypes // instead of displaying them (https://bugzilla.mozilla.org/1319262) resp.headers_mut() .insert(header::CONTENT_TYPE, "text/plain".parse().unwrap()); } else { resp.headers_mut() .insert(header::CONTENT_TYPE, mime.to_string().parse().unwrap()); } } Ok(resp) } fn run_blob( entr: TreeEntry, _params: ActionParam, _controller: &C, ctx: Context, parts: &Parts, ) -> Result { if ctx.path.extension() != Some("html") { return Err(Error::BadRequest( "run action only available for .html files".into(), )); } let mut resp = build_raw_response(entr, &ctx, parts)?; resp.headers_mut() .insert(header::CONTENT_TYPE, "text/html".parse().unwrap()); // We want users to be able to view .html applications of other users // without having to worry about the application accessing their private // files. So we set the CSP: sandbox header which makes browsers treat // the page as a unique origin as per the same-origin policy. resp.headers_mut().insert( header::CONTENT_SECURITY_POLICY, "sandbox allow-scripts;".parse().unwrap(), ); Ok(resp) } fn move_blob( _entr: TreeEntry, _params: ActionParam, controller: &C, ctx: Context, parts: &Parts, ) -> Result { if !controller.may_move_path(&ctx, parts) { return Err(Error::Unauthorized( "you are not authorized to move this file".into(), )); } let filename = ctx.path.file_name().unwrap(); return forms::move_form( filename, &forms::MoveForm { dest: ctx.path.as_str().to_owned(), msg: None, }, None, controller, &ctx, parts, ); } fn remove_blob( params: ActionParam, controller: &C, ctx: Context, parts: &Parts, ) -> Result { if !controller.may_move_path(&ctx, parts) { return Err(Error::Unauthorized( "you are not authorized to remove this file".into(), )); } let filename = ctx.path.file_name().unwrap(); let page = Page { title: format!("Remove {}", filename), header: action_links(¶ms.action, controller, &ctx, parts), body: "
\ \
" .into(), ..Default::default() }; Ok(page.into()) } pub fn view_tree( tree: Option, controller: &C, ctx: &Context, parts: &Parts, ) -> Result { let mut page = Page { title: ctx.path.as_str().to_string(), ..Default::default() }; page.body.push_str("
    "); if parts.uri.path() != "/" { page.body .push_str("
  • ../
  • "); } if let Some(tree) = &tree { let mut entries: Vec<_> = tree.iter().collect(); entries.sort_by_key(|a| a.kind().unwrap().raw()); for entry in entries { // if the name isn't valid utf8 we skip the entry if let Some(name) = entry.name() { if entry.kind() == Some(ObjectType::Tree) { page.body.push_str(&format!( "
  • {0}/
  • ", html_escape(name) )); } else { page.body.push_str(&format!( "
  • {0}
  • ", html_escape(name) )); } } } } page.body.push_str("
"); controller.before_return_tree_page(&mut page, tree, ctx, parts); Ok(page.into()) }