//! 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())
}