diff --git a/src/app/browser/window/tab/item/identity/gemini.rs b/src/app/browser/window/tab/item/identity/gemini.rs index 967cbbc5..16addca5 100644 --- a/src/app/browser/window/tab/item/identity/gemini.rs +++ b/src/app/browser/window/tab/item/identity/gemini.rs @@ -23,7 +23,7 @@ impl Gemini { /// Create new `Self` for given Profile pub fn new(profile: Rc, action: Rc, auth_uri: Uri) -> Self { // Init widget - let widget = Rc::new(Widget::new()); + let widget = Rc::new(Widget::new(profile.clone())); // Init shared components let auth_url = auth_uri.to_string(); diff --git a/src/app/browser/window/tab/item/identity/gemini/widget.rs b/src/app/browser/window/tab/item/identity/gemini/widget.rs index 5657a6f7..f2a787ba 100644 --- a/src/app/browser/window/tab/item/identity/gemini/widget.rs +++ b/src/app/browser/window/tab/item/identity/gemini/widget.rs @@ -4,6 +4,7 @@ pub mod form; use action::Action; use form::{list::item::value::Value, Form}; +use crate::profile::Profile; use adw::{ prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual}, AlertDialog, ResponseAppearance, @@ -32,12 +33,12 @@ impl Widget { // Constructors /// Create new `Self` - pub fn new() -> Self { + pub fn new(profile: Rc) -> Self { // Init actions let action = Rc::new(Action::new()); // Init child container - let form = Rc::new(Form::new(action.clone())); + let form = Rc::new(Form::new(profile, action.clone())); // Init main `GObject` let gobject = AlertDialog::builder() diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form.rs index 5be033a5..f0f8ca02 100644 --- a/src/app/browser/window/tab/item/identity/gemini/widget/form.rs +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form.rs @@ -9,6 +9,7 @@ use name::Name; use save::Save; use super::Action; +use crate::profile::Profile; use gtk::{prelude::BoxExt, Box, Orientation}; use std::rc::Rc; @@ -17,7 +18,7 @@ pub struct Form { pub file: Rc, pub list: Rc, pub name: Rc, - pub save: Rc, + // pub save: Rc, pub gobject: Box, } @@ -25,12 +26,12 @@ impl Form { // Constructors /// Create new `Self` - pub fn new(action: Rc) -> Self { + pub fn new(profile: Rc, action: Rc) -> Self { // Init components let file = Rc::new(File::new(action.clone())); let list = Rc::new(List::new()); let name = Rc::new(Name::new(action.clone())); - let save = Rc::new(Save::new(action.clone())); + let save = Rc::new(Save::new(profile)); // Init main container let gobject = Box::builder().orientation(Orientation::Vertical).build(); @@ -48,18 +49,18 @@ impl Form { let update = action.update.clone(); move |item| { // Change name entry visibility - name.show(matches!(item, Value::GenerateNewAuth)); + name.update(matches!(item, Value::GenerateNewAuth)); // Change file choose button visibility - file.show(matches!(item, Value::ImportPem)); + file.update(matches!(item, Value::ImportPem)); - // Change export button visibility, update holder @TODO + // Change export button visibility by update it holder value match item { - Value::ProfileIdentityGeminiId(id) => { - // save.show(Some(id)); + Value::ProfileIdentityGeminiId(value) => { + save.update(Some(value)); } _ => { - // save.show(None); + save.update(None); } } @@ -75,7 +76,7 @@ impl Form { file, list, name, - save, + // save, } } diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/file.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/file.rs index ce1b3b41..f440860f 100644 --- a/src/app/browser/window/tab/item/identity/gemini/widget/form/file.rs +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/file.rs @@ -101,7 +101,7 @@ impl File { /// Change visibility status /// * grab focus on `is_visible` - pub fn show(&self, is_visible: bool) { + pub fn update(&self, is_visible: bool) { self.gobject.set_visible(is_visible); if is_visible { self.gobject.grab_focus(); diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/name.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/name.rs index 144421c4..ca1f399e 100644 --- a/src/app/browser/window/tab/item/identity/gemini/widget/form/name.rs +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/name.rs @@ -39,7 +39,7 @@ impl Name { /// Change visibility status /// * grab focus on `is_visible` - pub fn show(&self, is_visible: bool) { + pub fn update(&self, is_visible: bool) { self.gobject.set_visible(is_visible); if is_visible { self.gobject.grab_focus(); diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/save.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/save.rs index ec105818..aa80c339 100644 --- a/src/app/browser/window/tab/item/identity/gemini/widget/form/save.rs +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/save.rs @@ -1,7 +1,7 @@ mod certificate; use certificate::Certificate; -use super::Action; +use crate::profile::Profile; use gtk::{ gio::{Cancellable, ListStore}, prelude::{ButtonExt, FileExt, WidgetExt}, @@ -13,7 +13,7 @@ const LABEL: &str = "Export to file.."; const MARGIN: i32 = 8; pub struct Save { - certificate: Rc>>, + profile_identity_gemini_id: Rc>>, pub gobject: Button, } @@ -21,9 +21,9 @@ impl Save { // Constructors /// Create new `Self` - pub fn new(action: Rc) -> Self { - // Init `Certificate` holder - let certificate = Rc::new(RefCell::new(None::)); + pub fn new(profile: Rc) -> Self { + // Init selected option holder + let profile_identity_gemini_id = Rc::new(RefCell::new(None)); // Init `GObject` let gobject = Button::builder() @@ -34,75 +34,91 @@ impl Save { // Init events gobject.connect_clicked({ - let certificate = certificate.clone(); + let profile_identity_gemini_id = profile_identity_gemini_id.clone(); let gobject = gobject.clone(); move |_| { - // Get certificate selected from holder - match certificate.borrow().as_ref() { - Some(certificate) => { - // Copy certificate holder values - let name = certificate.name.clone(); - let data = certificate.data.clone(); - + // Get selected identity from holder + match profile_identity_gemini_id.borrow().as_ref() { + Some(profile_identity_gemini_id) => { // Lock open button (prevent double click) gobject.set_sensitive(false); - // Init file filters related with PEM extension - let filters = ListStore::new::(); + // Create PEM file based on option ID selected + match Certificate::new(profile.clone(), *profile_identity_gemini_id) { + Ok(certificate) => { + // Init file filters related with PEM extension + let filters = ListStore::new::(); - let filter_all = FileFilter::new(); - filter_all.add_pattern("*.*"); - filter_all.set_name(Some("All")); - filters.append(&filter_all); + let filter_all = FileFilter::new(); + filter_all.add_pattern("*.*"); + filter_all.set_name(Some("All")); + filters.append(&filter_all); - let filter_pem = FileFilter::new(); - filter_pem.add_mime_type("application/x-x509-ca-cert"); - filter_pem.set_name(Some("Certificate (*.pem)")); - filters.append(&filter_pem); + let filter_pem = FileFilter::new(); + filter_pem.add_mime_type("application/x-x509-ca-cert"); + filter_pem.set_name(Some("Certificate (*.pem)")); + filters.append(&filter_pem); - // Init file dialog - FileDialog::builder() - .default_filter(&filter_pem) - .filters(&filters) - .initial_name(format!("{name}.pem")) - .build() - .save(None::<&Window>, None::<&Cancellable>, { - let gobject = gobject.clone(); - move |result| { - match result { - Ok(file) => match file.path() { - Some(path) => match File::create(&path) { - Ok(mut destination) => { - match destination.write_all(data.as_bytes()) { - Ok(_) => { - // @TODO - gobject.set_css_classes(&["success"]); - gobject.set_label(&format!( - "Saved to {}", - path.to_string_lossy() - )) + // Init file dialog + FileDialog::builder() + .default_filter(&filter_pem) + .filters(&filters) + .initial_name(format!("{}.pem", certificate.name)) + .build() + .save(None::<&Window>, None::<&Cancellable>, { + let gobject = gobject.clone(); + move |result| { + match result { + Ok(file) => match file.path() { + Some(path) => match File::create(&path) { + Ok(mut destination) => { + match destination.write_all( + certificate.data.as_bytes(), + ) { + Ok(_) => { + gobject.set_css_classes(&[ + "success", + ]); + gobject.set_label(&format!( + "Saved to {}", + path.to_string_lossy() + )) + } + Err(e) => { + gobject.set_css_classes(&[ + "error", + ]); + gobject + .set_label(&e.to_string()) + } + } } - Err(reason) => { + Err(e) => { gobject.set_css_classes(&["error"]); - gobject.set_label(&reason.to_string()) + gobject.set_label(&e.to_string()) } + }, + None => { + gobject.set_css_classes(&["warning"]); + gobject.set_label( + "Could not init destination path", + ) } + }, + Err(e) => { + gobject.set_css_classes(&["warning"]); + gobject.set_label(e.message()) } - Err(reason) => { - gobject.set_css_classes(&["error"]); - gobject.set_label(&reason.to_string()) - } - }, - None => todo!(), - }, - Err(reason) => { - gobject.set_css_classes(&["warning"]); - gobject.set_label(reason.message()) + } + gobject.set_sensitive(true); // unlock } - } - gobject.set_sensitive(true); // unlock - } - }); + }); + } + Err(e) => { + gobject.set_css_classes(&["error"]); + gobject.set_label(&e.to_string()) + } + } } None => todo!(), // unexpected } @@ -111,16 +127,25 @@ impl Save { // Return activated `Self` Self { - certificate, + profile_identity_gemini_id, gobject, } } // Actions - /// Change visibility status - /// * grab focus on `is_visible` - pub fn show(&self, is_visible: bool) { - self.gobject.set_visible(is_visible) + /// Update `profile_identity_gemini_id` holder, + /// toggle visibility depending on given value + pub fn update(&self, profile_identity_gemini_id: Option) { + self.gobject.set_visible(match profile_identity_gemini_id { + Some(value) => { + self.profile_identity_gemini_id.replace(Some(value)); + true + } + None => { + self.profile_identity_gemini_id.replace(None); + false + } + }) } } diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/save/certificate.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/save/certificate.rs index 5ad06d70..788ff100 100644 --- a/src/app/browser/window/tab/item/identity/gemini/widget/form/save/certificate.rs +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/save/certificate.rs @@ -1,4 +1,38 @@ +mod error; +pub use error::Error; + +use crate::profile::Profile; +use gtk::{gio::TlsCertificate, prelude::TlsCertificateExt}; +use std::rc::Rc; + +/// Certificate details holder for export to file action pub struct Certificate { - pub name: String, pub data: String, + pub name: String, +} + +impl Certificate { + // Constructors + + /// Create new `Self` + pub fn new(profile: Rc, profile_identity_gemini_id: i64) -> Result { + match profile + .identity + .gemini + .database + .record(profile_identity_gemini_id) + { + Ok(record) => match record { + Some(identity) => match TlsCertificate::from_pem(&identity.pem) { + Ok(certificate) => Ok(Self { + data: identity.pem, + name: certificate.subject_name().unwrap().replace("CN=", ""), + }), + Err(reason) => Err(Error::TlsCertificate(reason)), + }, + None => Err(Error::NotFound(profile_identity_gemini_id)), + }, + Err(reason) => Err(Error::Database(reason)), + } + } } diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/save/certificate/error.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/save/certificate/error.rs new file mode 100644 index 00000000..0aa6bcec --- /dev/null +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/save/certificate/error.rs @@ -0,0 +1,25 @@ +use gtk::glib; +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Database(sqlite::Error), + NotFound(i64), + TlsCertificate(glib::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Database(e) => { + write!(f, "Database error: {e}") + } + Self::NotFound(profile_identity_gemini_id) => { + write!(f, "Record for `{profile_identity_gemini_id}` not found") + } + Self::TlsCertificate(e) => { + write!(f, "TLS certificate error: {e}") + } + } + } +} diff --git a/src/profile/identity/gemini/database.rs b/src/profile/identity/gemini/database.rs index 2115bb3c..73d7e553 100644 --- a/src/profile/identity/gemini/database.rs +++ b/src/profile/identity/gemini/database.rs @@ -45,6 +45,21 @@ impl Database { } } + /// Get single record match `id` + pub fn record(&self, id: i64) -> Result, Error> { + let readable = self.connection.read().unwrap(); + let tx = readable.unchecked_transaction()?; + let records = select(&tx, *self.profile_identity_id)?; // @TODO single record query + + for record in records { + if record.id == id { + return Ok(Some(record)); + } + } + + Ok(None) + } + /// Get all records match current `profile_identity_id` pub fn records(&self) -> Result, Error> { let readable = self.connection.read().unwrap(); // @TODO