use qframe::prelude::*;
use qframe::widgets::{Field as FormFieldWidget, Form, FormErrors, Modal, TextInput};
use super::Msg;
use crate::config::Provider;
use crate::provider::{Key, KeyError};
pub const FIRST: &str = "provider-address";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Field {
Address,
Model,
KeyFile,
KeyHeader,
}
impl Field {
const ALL: [Self; 4] = [Self::Address, Self::Model, Self::KeyFile, Self::KeyHeader];
fn id(self) -> &'static str {
match self {
Self::Address => FIRST,
Self::Model => "provider-model",
Self::KeyFile => "provider-key-file",
Self::KeyHeader => "provider-key-header",
}
}
fn label(self) -> String {
match self {
Self::Address => t!("provider.address"),
Self::Model => t!("provider.model"),
Self::KeyFile => t!("provider.key-file"),
Self::KeyHeader => t!("provider.key-header"),
}
}
fn hint(self) -> String {
match self {
Self::Address => t!("provider.address-hint"),
Self::Model => t!("provider.model-hint"),
Self::KeyFile => t!("provider.key-file-hint"),
Self::KeyHeader => t!("provider.key-header-hint"),
}
}
fn needed(self) -> bool {
!matches!(self, Self::KeyHeader)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderForm {
provider: Provider,
errors: FormErrors,
}
impl ProviderForm {
#[must_use]
pub fn from_provider(provider: &Provider) -> Self {
Self { provider: provider.clone(), errors: FormErrors::new() }
}
pub fn set(&mut self, field: Field, value: String) {
match field {
Field::Address => self.provider.address = value,
Field::Model => self.provider.model = value,
Field::KeyFile => self.provider.key_file = value,
Field::KeyHeader => self.provider.key_header = value,
}
}
fn value(&self, field: Field) -> &str {
match field {
Field::Address => &self.provider.address,
Field::Model => &self.provider.model,
Field::KeyFile => &self.provider.key_file,
Field::KeyHeader => &self.provider.key_header,
}
}
pub fn check(&mut self) -> Option<Provider> {
let provider = Provider {
address: self.provider.address.trim().to_owned(),
model: self.provider.model.trim().to_owned(),
key_file: self.provider.key_file.trim().to_owned(),
key_header: self.provider.key_header.trim().to_owned(),
source: crate::config::Source::Settings,
};
let mut errors = FormErrors::new();
for field in Field::ALL.into_iter().filter(|field| field.needed()) {
let value = match field {
Field::Address => &provider.address,
Field::Model => &provider.model,
_ => &provider.key_file,
};
errors.check(field.id(), !value.is_empty(), t!("provider.empty"));
}
if !provider.key_file.is_empty()
&& let Err(error) = Key::from_file(&provider.key_path())
{
errors.set(Field::KeyFile.id(), refusal(&error));
}
self.errors = errors;
self.errors.is_empty().then_some(provider)
}
#[must_use]
pub fn errors(&self) -> &FormErrors {
&self.errors
}
pub fn failed(&mut self, error: &std::io::Error) {
self.errors = FormErrors::new();
self.errors.set(Field::Address.id(), t!("provider.unsaved", reason = error.to_string()));
}
fn warning(&self, field: Field) -> Option<String> {
match field {
Field::Address => plain_http(&self.provider.address).then(|| t!("provider.plain-http")),
Field::KeyFile => {
let path = self.provider.key_path();
(!self.provider.key_file.trim().is_empty() && others_can_read(&path))
.then(|| t!("provider.key-file-open"))
}
_ => None,
}
}
pub fn make_key_file_private(&mut self) {
use std::os::unix::fs::PermissionsExt as _;
let path = self.provider.key_path();
if let Err(error) = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) {
self.errors.set(
Field::KeyFile.id(),
t!("provider.key-unreadable", path = path.display().to_string(), reason = error.to_string()),
);
}
}
pub fn view(&self, ui: &mut View<'_, Msg>) {
let dialog = Modal::new()
.title(t!("provider.title"))
.width(72)
.on_close(Msg::CloseProvider)
.action(Button::new(t!("provider.cancel")).on_press(Msg::CloseProvider))
.action(Button::new(t!("provider.save")).variant("primary").on_press(Msg::SaveProvider));
ui.add_with(dialog, |ui| {
ui.add(Text::new(t!("provider.intro")).role("secondary"));
Form::new().show(ui, |fields| {
for field in Field::ALL {
let widget = FormFieldWidget::new(field.label())
.required(field.needed())
.hint(field.hint())
.warning(self.warning(field))
.error(self.errors.get(field.id()));
fields.field(widget, |ui| {
ui.add(
TextInput::new(self.value(field))
.invalid(self.errors.has(field.id()))
.on_change(move |value| Msg::Form(field, value))
.on_submit(|_| Msg::SaveProvider),
)
.id(field.id())
.fill_width();
});
if field == Field::KeyFile && self.warning(field).is_some() {
fields
.ui()
.add(Button::new(t!("provider.make-private")).on_press(Msg::PrivateKeyFile))
.id("provider-make-private");
}
}
});
});
}
}
fn refusal(error: &KeyError) -> String {
match error {
KeyError::Unreadable { path, reason } => {
t!("provider.key-unreadable", path = path.display().to_string(), reason = reason.clone())
}
KeyError::NotAKey { path } => t!("provider.not-a-key", path = path.display().to_string()),
}
}
fn plain_http(address: &str) -> bool {
let address = address.trim();
let Some(rest) = address.get(..7).filter(|scheme| scheme.eq_ignore_ascii_case("http://")).map(|_| &address[7..])
else {
return false;
};
let authority = rest.split(['/', '?', '#']).next().unwrap_or_default();
!crate::config::loopback(crate::config::host(authority))
}
fn others_can_read(path: &std::path::Path) -> bool {
use std::os::unix::fs::PermissionsExt as _;
std::fs::metadata(path).is_ok_and(|meta| meta.permissions().mode() & 0o044 != 0)
}