pub use crate::forms::generic::GenericField;
use crate::forms::{
base::{CommonFieldConfig, FieldConfig, FormField, TextConfig},
options::LengthConstraint,
};
use crate::utils::{
aliases::ATera,
password::{PasswordConfig, PasswordService},
trad::{t, tf},
};
use async_trait::async_trait;
use serde::Serialize;
use validator::{ValidateEmail, ValidateUrl};
#[derive(Clone, Serialize, Debug)]
pub struct TextField {
pub base: FieldConfig,
pub config: TextConfig,
pub format: SpecialFormat,
pub hash_password: bool, }
#[derive(Clone, Debug, Serialize, PartialEq)]
pub enum SpecialFormat {
None,
Email,
Url,
Password,
RichText,
Csrf,
Phone,
}
impl CommonFieldConfig for TextField {
fn get_field_config(&self) -> &FieldConfig {
&self.base
}
fn get_field_config_mut(&mut self) -> &mut FieldConfig {
&mut self.base
}
}
impl TextField {
fn create(name: &str, type_field: &str, format: SpecialFormat) -> Self {
Self {
base: FieldConfig::new(name, type_field, "base_string.html"),
config: TextConfig::default(),
format,
hash_password: true,
}
}
pub fn no_hash(mut self) -> Self {
self.hash_password = false;
self
}
pub fn min_length(mut self, min: u32, msg: &str) -> Self {
self.config.min_length = Some(LengthConstraint {
value: min,
message: (!msg.is_empty()).then(|| msg.to_string()),
});
self
}
pub fn max_length(mut self, max: u32, msg: &str) -> Self {
self.config.max_length = Some(LengthConstraint {
value: max,
message: (!msg.is_empty()).then(|| msg.to_string()),
});
self
}
pub fn text(name: &str) -> Self {
Self::create(name, "text", SpecialFormat::None)
}
pub fn textarea(name: &str) -> Self {
Self::create(name, "textarea", SpecialFormat::None)
}
pub fn richtext(name: &str) -> Self {
Self::create(name, "richtext", SpecialFormat::RichText)
}
pub fn password(name: &str) -> Self {
Self::create(name, "password", SpecialFormat::Password)
}
pub fn email(name: &str) -> Self {
let mut field = Self::create(name, "email", SpecialFormat::Email);
field.base.value = field.base.value.to_lowercase();
field
}
pub fn url(name: &str) -> Self {
Self::create(name, "url", SpecialFormat::Url)
}
pub fn phone(name: &str) -> Self {
Self::create(name, "tel", SpecialFormat::Phone)
}
pub fn required(mut self) -> Self {
self.set_required(true, None);
self
}
pub fn label(mut self, label: &str) -> Self {
self.base.label = label.to_string();
self
}
pub fn placeholder(mut self, p: &str) -> Self {
self.set_placeholder(p);
self
}
pub fn readonly(mut self, msg: &str) -> Self {
self.set_readonly(true, Some(msg));
self
}
pub fn disabled(mut self, msg: &str) -> Self {
self.set_disabled(true, Some(msg));
self
}
pub fn rows(mut self, rows: usize) -> Self {
self.base
.extra_context
.insert("rows".to_string(), serde_json::json!(rows));
self
}
}
#[async_trait]
impl FormField for TextField {
fn bounds(&self) -> crate::forms::base::FieldBounds {
crate::forms::base::FieldBounds {
min_length: self.config.min_length.as_ref().map(|l| l.value),
max_length: self.config.max_length.as_ref().map(|l| l.value),
..Default::default()
}
}
fn set_value(&mut self, value: &str) {
let cleaned = match self.format {
SpecialFormat::Password | SpecialFormat::Csrf => value.to_string(),
SpecialFormat::RichText => crate::utils::sanitizer::sanitize_rich(value),
_ => crate::utils::sanitizer::sanitize_strict(value),
};
self.base.value = cleaned;
}
async fn validate(&mut self) -> bool {
let mut val = self.base.value.trim().to_string();
if self.base.is_required.choice && val.is_empty() {
let msg = self
.base
.is_required
.message
.clone()
.unwrap_or_else(|| t("forms.required").to_string());
self.set_error(msg);
return false;
}
if val.is_empty() {
return true;
}
if let Some(limits) = &self.config.min_length {
let count: u32 = val.chars().count() as u32;
if count < limits.value {
let msg = limits
.message
.clone()
.unwrap_or_else(|| tf("forms.too_short", &[&limits.value]));
self.set_error(msg);
return false;
}
}
if let Some(limits) = &self.config.max_length {
let count: u32 = val.chars().count() as u32;
if count > limits.value {
let msg = limits
.message
.clone()
.unwrap_or_else(|| tf("forms.too_long", &[&limits.value]));
self.set_error(msg);
return false;
}
}
match &self.format {
SpecialFormat::Email if !val.validate_email() => {
self.set_error(t("forms.email_invalid").to_string());
return false;
}
SpecialFormat::Email => {
val = val.to_lowercase();
}
SpecialFormat::Url if !val.validate_url() => {
self.set_error(t("forms.url_invalid").to_string());
return false;
}
SpecialFormat::Phone => {
let digit_count = val.chars().filter(|c| c.is_ascii_digit()).count();
let has_only_valid_chars = val
.chars()
.all(|c| c.is_ascii_digit() || matches!(c, '+' | ' ' | '-' | '(' | ')'));
let plus_only_at_start = val.match_indices('+').all(|(i, _)| i == 0);
if !has_only_valid_chars || !plus_only_at_start || !(7..=15).contains(&digit_count)
{
self.set_error(t("forms.phone_invalid").to_string());
return false;
}
}
_ => {}
}
self.base.value = val;
self.clear_error();
true
}
async fn finalize(&mut self) -> Result<(), String> {
if let SpecialFormat::Password = &self.format
&& self.hash_password
&& !self.base.value.is_empty()
{
let config = crate::utils::password::password_get();
if let PasswordConfig::Auto(_) = config {
let service = PasswordService::new(config);
if !service.is_already_hashed(&self.base.value) {
self.base.value = service
.hash(&self.base.value)
.map_err(|e| tf("forms.hash_error", &[&e.to_string()]).to_string())?;
}
}
}
Ok(())
}
fn render(&self, tera: &ATera) -> Result<String, String> {
let mut context = self.base_context();
let mut base_data = self.base.clone();
if base_data.is_password() {
base_data.value = String::new();
}
context.insert("field", &base_data);
context.insert("input_type", &self.base.type_field);
if let Some(l) = &self.config.min_length {
context.insert("min_length", &l.value);
}
if let Some(l) = &self.config.max_length {
context.insert("max_length", &l.value);
}
if let Some(rows) = self.base.extra_context.get("rows").and_then(|r| r.as_u64()) {
context.insert("rows", &rows);
}
tera.render(&self.base.template_name, &context)
.map_err(|e| {
tf(
"forms.finalize_error",
&[&self.base.template_name, &e.to_string()],
)
.to_string()
})
}
}