//! HTML pages. Each page renders its own content template inside the //! shared site layout. pub mod file; pub mod index; pub mod repo; use anyhow::Error; use poem::{ IntoResponse, Response, error::NotFoundError, http::{StatusCode, header}, web::Html, }; use sailfish::TemplateOnce; use crate::{ App, repos::{RepoName, RepoPath}, }; // What a page of the site may do in a browser: load its own stylesheet, // and images from wherever a README refers to them. No script, frames, // forms or base URL, so that a flaw in the sanitiser or a slip in a // template cannot become script running on the site's origin. const PAGE_POLICY: &str = "default-src 'none'; style-src 'self'; img-src 'self' https: http:; \ base-uri 'none'; form-action 'none'; frame-ancestors 'none'"; #[derive(TemplateOnce)] #[template(path = "site/layout.stpl.html")] struct Layout { site_name: String, title: String, content: C, } /// Renders page content inside the site layout. pub fn page( app: &App, title: impl Into, content: impl TemplateOnce, ) -> Result { let layout = Layout { site_name: app.config.site_name.clone(), title: title.into(), content, }; Ok(html(layout.render_once()?)) } // An HTML document under the page policy. fn html(body: String) -> Response { Html(body) .with_header(header::CONTENT_SECURITY_POLICY, PAGE_POLICY) .into_response() } #[derive(TemplateOnce)] #[template(path = "pages/error.stpl.html")] struct ErrorPage { reason: String, detail: String, } /// The page for a request that failed, in the site's own layout. A /// repository that is private is reported the same way as one that does /// not exist, so the site never confirms a private name; the details of a /// server-side failure stay in the journal. pub fn error(app: &App, err: &poem::Error) -> Response { let status = err.status(); let reason = status.canonical_reason().unwrap_or("Error").to_owned(); let detail = if status == StatusCode::NOT_FOUND { "There is no such page or public repository.".to_owned() } else if status.is_client_error() { err.to_string() } else { "Something went wrong on the server.".to_owned() }; let content = ErrorPage { reason: reason.clone(), detail, }; let mut response = page(app, reason.clone(), content).unwrap_or_else(|_| html(reason)); response.set_status(status); response } /// A repository name from a URL. An invalid name is reported as not /// found, like a private one, so the site never says which names would /// have been valid. pub fn repo_name(text: &str) -> poem::Result { RepoName::parse(text).map_err(|_| NotFoundError.into()) } /// A path within a repository from a URL, likewise. pub fn repo_path(text: &str) -> poem::Result { RepoPath::parse(text).map_err(|_| NotFoundError.into()) } /// A lookup that found nothing is a page that does not exist. pub fn found(value: Option) -> poem::Result { value.ok_or_else(|| NotFoundError.into()) }