pages.rs raw

//! 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<C: TemplateOnce> {
    site_name: String,
    title: String,
    content: C,
}

/// Renders page content inside the site layout.
pub fn page(
    app: &App,
    title: impl Into<String>,
    content: impl TemplateOnce,
) -> Result<Response, Error> {
    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> {
    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> {
    RepoPath::parse(text).map_err(|_| NotFoundError.into())
}

/// A lookup that found nothing is a page that does not exist.
pub fn found<T>(value: Option<T>) -> poem::Result<T> {
    value.ok_or_else(|| NotFoundError.into())
}