1use qframe::prelude::*;
4use qframe::widgets::{Field as FormFieldWidget, Form, FormErrors, Modal, TextInput};
5
6use super::Msg;
7use crate::config::Provider;
8use crate::provider::{Key, KeyError};
9
10pub const FIRST: &str = "provider-address";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Field {
16 Address,
18 Model,
20 KeyFile,
22 KeyHeader,
24}
25
26impl Field {
27 const ALL: [Self; 4] = [Self::Address, Self::Model, Self::KeyFile, Self::KeyHeader];
28
29 fn id(self) -> &'static str {
30 match self {
31 Self::Address => FIRST,
32 Self::Model => "provider-model",
33 Self::KeyFile => "provider-key-file",
34 Self::KeyHeader => "provider-key-header",
35 }
36 }
37
38 fn label(self) -> String {
39 match self {
40 Self::Address => t!("provider.address"),
41 Self::Model => t!("provider.model"),
42 Self::KeyFile => t!("provider.key-file"),
43 Self::KeyHeader => t!("provider.key-header"),
44 }
45 }
46
47 fn hint(self) -> String {
48 match self {
49 Self::Address => t!("provider.address-hint"),
50 Self::Model => t!("provider.model-hint"),
51 Self::KeyFile => t!("provider.key-file-hint"),
52 Self::KeyHeader => t!("provider.key-header-hint"),
53 }
54 }
55
56 fn needed(self) -> bool {
58 !matches!(self, Self::KeyHeader)
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct ProviderForm {
66 provider: Provider,
67 errors: FormErrors,
68}
69
70impl ProviderForm {
71 #[must_use]
73 pub fn from_provider(provider: &Provider) -> Self {
74 Self { provider: provider.clone(), errors: FormErrors::new() }
75 }
76
77 pub fn set(&mut self, field: Field, value: String) {
79 match field {
80 Field::Address => self.provider.address = value,
81 Field::Model => self.provider.model = value,
82 Field::KeyFile => self.provider.key_file = value,
83 Field::KeyHeader => self.provider.key_header = value,
84 }
85 }
86
87 fn value(&self, field: Field) -> &str {
88 match field {
89 Field::Address => &self.provider.address,
90 Field::Model => &self.provider.model,
91 Field::KeyFile => &self.provider.key_file,
92 Field::KeyHeader => &self.provider.key_header,
93 }
94 }
95
96 pub fn check(&mut self) -> Option<Provider> {
99 let provider = Provider {
100 address: self.provider.address.trim().to_owned(),
101 model: self.provider.model.trim().to_owned(),
102 key_file: self.provider.key_file.trim().to_owned(),
103 key_header: self.provider.key_header.trim().to_owned(),
104 source: crate::config::Source::Settings,
105 };
106 let mut errors = FormErrors::new();
107 for field in Field::ALL.into_iter().filter(|field| field.needed()) {
108 let value = match field {
109 Field::Address => &provider.address,
110 Field::Model => &provider.model,
111 _ => &provider.key_file,
112 };
113 errors.check(field.id(), !value.is_empty(), t!("provider.empty"));
114 }
115 if !provider.key_file.is_empty()
116 && let Err(error) = Key::from_file(&provider.key_path())
117 {
118 errors.set(Field::KeyFile.id(), refusal(&error));
119 }
120 self.errors = errors;
121 self.errors.is_empty().then_some(provider)
122 }
123
124 #[must_use]
126 pub fn errors(&self) -> &FormErrors {
127 &self.errors
128 }
129
130 pub fn failed(&mut self, error: &std::io::Error) {
132 self.errors = FormErrors::new();
133 self.errors.set(Field::Address.id(), t!("provider.unsaved", reason = error.to_string()));
134 }
135
136 fn warning(&self, field: Field) -> Option<String> {
140 match field {
141 Field::Address => plain_http(&self.provider.address).then(|| t!("provider.plain-http")),
142 Field::KeyFile => {
143 let path = self.provider.key_path();
144 (!self.provider.key_file.trim().is_empty() && others_can_read(&path))
145 .then(|| t!("provider.key-file-open"))
146 }
147 _ => None,
148 }
149 }
150
151 pub fn make_key_file_private(&mut self) {
154 use std::os::unix::fs::PermissionsExt as _;
155 let path = self.provider.key_path();
156 if let Err(error) = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) {
157 self.errors.set(
158 Field::KeyFile.id(),
159 t!("provider.key-unreadable", path = path.display().to_string(), reason = error.to_string()),
160 );
161 }
162 }
163
164 pub fn view(&self, ui: &mut View<'_, Msg>) {
166 let dialog = Modal::new()
167 .title(t!("provider.title"))
168 .width(72)
169 .on_close(Msg::CloseProvider)
170 .action(Button::new(t!("provider.cancel")).on_press(Msg::CloseProvider))
171 .action(Button::new(t!("provider.save")).variant("primary").on_press(Msg::SaveProvider));
172 ui.add_with(dialog, |ui| {
173 ui.add(Text::new(t!("provider.intro")).role("secondary"));
174 Form::new().show(ui, |fields| {
175 for field in Field::ALL {
176 let widget = FormFieldWidget::new(field.label())
177 .required(field.needed())
178 .hint(field.hint())
179 .warning(self.warning(field))
180 .error(self.errors.get(field.id()));
181 fields.field(widget, |ui| {
182 ui.add(
183 TextInput::new(self.value(field))
184 .invalid(self.errors.has(field.id()))
185 .on_change(move |value| Msg::Form(field, value))
186 .on_submit(|_| Msg::SaveProvider),
187 )
188 .id(field.id())
189 .fill_width();
190 });
191 if field == Field::KeyFile && self.warning(field).is_some() {
193 fields
194 .ui()
195 .add(Button::new(t!("provider.make-private")).on_press(Msg::PrivateKeyFile))
196 .id("provider-make-private");
197 }
198 }
199 });
200 });
201 }
202}
203
204fn refusal(error: &KeyError) -> String {
206 match error {
207 KeyError::Unreadable { path, reason } => {
208 t!("provider.key-unreadable", path = path.display().to_string(), reason = reason.clone())
209 }
210 KeyError::NotAKey { path } => t!("provider.not-a-key", path = path.display().to_string()),
211 }
212}
213
214fn plain_http(address: &str) -> bool {
217 let address = address.trim();
218 let Some(rest) = address.get(..7).filter(|scheme| scheme.eq_ignore_ascii_case("http://")).map(|_| &address[7..])
219 else {
220 return false;
221 };
222 let authority = rest.split(['/', '?', '#']).next().unwrap_or_default();
223 !crate::config::loopback(crate::config::host(authority))
224}
225
226fn others_can_read(path: &std::path::Path) -> bool {
228 use std::os::unix::fs::PermissionsExt as _;
229 std::fs::metadata(path).is_ok_and(|meta| meta.permissions().mode() & 0o044 != 0)
230}