use std::collections::{HashMap, HashSet};
use topcoat::{
Result,
context::Cx,
router::{Body, error::forbidden},
view::BoxView,
};
use super::{
super::{actions::load_viewable, gate::gate},
render::{FormChrome, render_form_page},
};
use crate::{
db::db,
form::{FieldErrors, RecordForm},
policy::Ability,
resource::{Mounted, Resource},
topcoat_compat::async_page,
};
pub(crate) struct FormParts {
pub(crate) values: HashMap<String, String>,
pub(crate) files: HashMap<String, crate::upload::StagedUpload>,
pub(crate) file_part_names: HashSet<String>,
}
pub(crate) const MAX_FORM_BYTES: usize = 10 * 1024 * 1024;
pub(super) fn reject_unknown_form_keys(
schema: &crate::schema::Schema,
values: &HashMap<String, String>,
) -> Result<(), topcoat::Error> {
let mut filtered = values.clone();
strip_transport_keys(schema, &mut filtered);
let unknown = schema.unknown_keys(&filtered);
if unknown.is_empty() {
Ok(())
} else {
Err(topcoat::router::error::bad_request(format!(
"unknown field(s): {}",
unknown.join(", ")
))
.into())
}
}
pub(crate) fn truthy(v: &str) -> bool {
v == "1" || v == "true"
}
pub(super) fn strip_transport_keys(
schema: &crate::schema::Schema,
values: &mut HashMap<String, String>,
) {
let declared: HashSet<&str> = schema.fields().map(crate::schema::Field::name).collect();
values.retain(|k, _| {
if k == crate::csrf::FIELD_NAME {
return declared.contains(k.as_str());
}
let field = k.strip_prefix("clear_").or_else(|| k.strip_prefix("keep_"));
match field {
Some(field) if schema.fields().any(|f| f.is_file() && f.name() == field) => {
declared.contains(k.as_str())
}
_ => true,
}
});
}
pub(super) fn drop_client_typed_uploads(
schema: &crate::schema::Schema,
file_part_names: &HashSet<String>,
values: &mut HashMap<String, String>,
) {
for field in schema.fields().filter(|field| field.is_file()) {
if !file_part_names.contains(field.name()) {
values.remove(field.name());
}
}
}
pub(super) async fn restore_pending_uploads(
cx: &Cx,
schema: &crate::schema::Schema,
values: &mut HashMap<String, String>,
) -> HashSet<String> {
let mut restored = HashSet::new();
for field in schema.fields().filter(|field| field.is_file()) {
let name = field.name();
let empty = values
.get(name)
.map(|value| value.trim().is_empty())
.unwrap_or(true);
let cleared = values
.get(&format!("clear_{name}"))
.is_some_and(|value| truthy(value));
if !empty || cleared {
continue;
}
let Some(candidate) = values.get(&format!("keep_{name}")) else {
continue;
};
let candidate = candidate.trim().to_string();
if candidate.is_empty() || !crate::upload::holds(cx, &candidate).await {
continue;
}
values.insert(name.to_string(), candidate);
restored.insert(name.to_string());
}
restored
}
pub(super) async fn rerender_invalid_form<'a, R: Resource>(
cx: &'a Cx,
resource: &Mounted<R>,
tx: toasty::Transaction<'_>,
chrome: FormChrome<'a>,
values: &HashMap<String, String>,
errors: &FieldErrors,
carried: &HashSet<String>,
) -> Result<BoxView<'a>> {
drop(tx);
render_form_page(cx, resource, chrome, values, errors, carried).await
}
pub(crate) fn resource_edit<R: Resource>(cx: &Cx, _body: Body) -> BoxView<'_> {
async_page(async move {
let resource = gate::<R>(cx)?;
let mut db = db(cx);
let record = load_viewable(cx, &resource, &mut db).await?;
if !resource.can(cx, Ability::Update(&record)) {
return Err(forbidden().into());
}
crate::csrf::ensure_token(cx);
let values = <R::Form as RecordForm>::hydrate(cx, &record);
let html = render_form_page(
cx,
&resource,
FormChrome::edit(cx, &resource, &record),
&values,
&FieldErrors::new(),
&HashSet::new(),
)
.await?;
Ok(html)
})
}
#[cfg(test)]
mod tests;