//! digital garden/wiki //! //! mod api; pub mod index; mod markup; pub use markup::PageFrontmatter; use std::{str::FromStr, sync::Arc}; use axum::{ Extension, Router, body::Body, extract::{Path, RawQuery}, http::{StatusCode, header}, response::Response, routing::{get, post}, }; use bempline::{Document, Options, options::IncludeMethod}; use camino::Utf8PathBuf; use markup::PageTemplate; use sessionhash::RequestContext; use somethings::auth::Session; use wikarden::{PathType, Wikarden}; use crate::{ Query, RuntimeError, config::Settings, extractor::Wrap, fs::{self, RequestPath, SafePath}, handler::{quark::markup::Source, temporary_redirect}, state::SplormState, }; /// nested `/quark` /// /// returns quark related routes pub fn routes() -> Router { Router::new() .route("/api/create/section", post(api::create_section)) .route("/api/create", post(api::create)) .route("/api/edit/section", post(api::edit_section)) .route("/api/edit", post(api::edit)) .route("/api/move", post(api::move_page)) .route("/api/delete", post(api::delete)) .route("/", get(index::index)) .route("/{*name}", get(page)) } /// redirect `/quark` to `/quark/` /// /// if I didn't do this, the `/quark` nesting didn't catch `/quark/`, /// for some reason, and it went down [`handler::static_path()`] pub async fn redirect_index_not_directory() -> Result { super::temporary_redirect("quark/".to_owned()).await } /// GET /quark/{*name} async fn page( Extension(wikarden): Extension>, Extension(settings): Extension, Path(name): Path, maybe_session: Option>, ctx: RequestContext, RawQuery(rquery): RawQuery, ) -> Result { tracing::info!("{ctx} GET /quark/{name}"); if let Some(rq) = rquery { if let Ok(q) = Query::from_str(&rq) { if maybe_session.is_none() { return Err(RuntimeError::Generic403); } if q.has("edit") { return map_edit(name, &wikarden, settings).await; } if q.has("move") { return page_move(name, &wikarden, settings).await; } if q.has("delete") { return page_delete(name, settings).await; } } } // If we're serving the index, redirect there if name == settings.wikarden_index() { return temporary_redirect(format!(".")).await; } // Figure out if we're a Page or a Section. If we're a Section, check for a // section home and redirect there if we have one let resolved_path = match wikarden.path_type(&name).await { Some(PathType::Page) => name, Some(PathType::Section) => { let data = wikarden.get_section_data(&name).await?; if let Some(home) = data.home() { return temporary_redirect(home.to_string()).await; } else { name } } None => name, }; PageTemplate::new(&settings, &wikarden, &resolved_path) .set_with_tools(maybe_session.is_some()) .set_can_be_deleted(true) .set_can_be_moved(true) .rendered_response() .await } async fn map_edit( page: String, wik: &Wikarden, settings: Settings, ) -> Result { match wik.path_type(&page).await { Some(PathType::Page) => page_edit(page, wik, settings).await, Some(PathType::Section) => section_edit(page, wik, settings).await, None => Err(RuntimeError::Generic404), } } /// handles page edits /// /// Returns a `Response` of the editing page /// /// [`page()`] calls into this if the `"edit"` query boolean-parameter is /// present and if authentication is successful. async fn page_edit( page: String, wik: &Wikarden, settings: Settings, ) -> Result { let template_path = settings.quark_template_dir().join("minimal.html"); let mut template = open_template_path(template_path, &settings).await?; let rp = RequestPath::verify_safe(settings.root(), "quark/edit.html")?; let edit = fs::read_file_to_string(rp.system_path()).await?; let mut page_template = open_template(&edit, rp.system_path(), &settings).await?; let src = match Source::from_wik(wik, &page).await { //FIXME: Make a variant for unfound wiki pages Err(_e) => return Err(RuntimeError::Generic404), Ok(src) => src, }; page_template.set("page-editing", page); page_template.set("page-content", src.content()); match src.frontmatter().description() { Some(desc) => page_template.set("og-description", desc), None => page_template.set("og-description", ""), } template.set("mainClass", "main-create"); template.set("quarkForm", "true"); template.set("main", page_template.compile()); Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "text/html") .body(Body::from(template.compile())) .map_err(|_| RuntimeError::Unknown) } async fn page_move( page: String, wik: &Wikarden, settings: Settings, ) -> Result { let template_path = settings.quark_template_dir().join("minimal.html"); let mut template = open_template_path(template_path, &settings).await?; let rp = RequestPath::verify_safe(settings.root(), "quark/move.html")?; let edit = fs::read_file_to_string(rp.system_path()).await?; let mut page_template = open_template(&edit, rp.system_path(), &settings).await?; if !wik.page_exists(&page).await? { //FIXME: Make a variant for unfound wiki pages return Err(RuntimeError::Generic404); }; page_template.set("page-moving", page); template.set("mainClass", "main-create"); template.set("quarkForm", "true"); template.set("main", page_template.compile()); Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "text/html") .body(Body::from(template.compile())) .map_err(|_| RuntimeError::Unknown) } /// handles page deletion /// /// Returns a `Response` of the deletion confirmation page /// /// [`page()`] calls into this if the `"delete"` query boolean-parameter is /// present and if authentication is successful async fn page_delete(page: String, settings: Settings) -> Result { let template_path = settings.quark_template_dir().join("minimal.html"); let mut template = open_template_path(template_path, &settings).await?; let rp = RequestPath::verify_safe(settings.root(), "quark/delete.html")?; let delete = fs::read_file_to_string(rp.system_path()).await?; let mut page_template = open_template(&delete, rp.system_path(), &settings).await?; page_template.set("page", page); template.set("mainClass", "main-create"); template.set("quarkForm", "true"); template.set("main", page_template.compile()); Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "text/html") .body(Body::from(template.compile())) .map_err(|_| RuntimeError::Unknown) } async fn section_edit( page: String, wik: &Wikarden, settings: Settings, ) -> Result { let template_path = settings.quark_template_dir().join("minimal.html"); let mut template = open_template_path(template_path, &settings).await?; let rp = RequestPath::verify_safe(settings.root(), "quark/edit-section.html")?; let edit = fs::read_file_to_string(rp.system_path()).await?; let mut page_template = open_template(&edit, rp.system_path(), &settings).await?; let data = match wik.get_section_data(&page).await { //FIXME: Make a variant for unfound wiki pages Err(_e) => return Err(RuntimeError::Generic404), Ok(data) => data, }; page_template.set("section-editing", page); page_template.set( "section-home", data.home().map(|w| w.to_string()).unwrap_or_default(), ); template.set("mainClass", "main-create"); template.set("quarkForm", "true"); template.set("main", page_template.compile()); Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "text/html") .body(Body::from(template.compile())) .map_err(|_| RuntimeError::Unknown) } /// gets quark template from the filesystem /// /// assumes quark templates are in the template directory under the `quark` sub-directory pub async fn open_template_path>( path: P, settings: &Settings, ) -> Result { let path = path.into(); let str = fs::unsafe_read_file_to_string(path.clone()).await?; let options = Options::new().include_path(IncludeMethod::Path( settings.quark_template_dir().into_std_path_buf(), )); Document::from_str(str, options).map_err(|perr| RuntimeError::template_parse_error(perr, path)) } /// creates quark template from a `&str` pub async fn open_template<'p>( content: &str, path: SafePath<'p>, settings: &Settings, ) -> Result { let options = Options::new().include_path(IncludeMethod::Path( settings.quark_template_dir().into_std_path_buf(), )); Document::from_str(content, options) .map_err(|perr| RuntimeError::template_parse_error(perr, path.to_path_buf())) }