Implement structured Android entry viewer (#99)
This commit is contained in:
1 parent
647dc1bb4b
commit
c0ba6d43d8
29 files changed
+1768
-78
No files matched your search
@@ -21,7 +21,7 @@ use crate::{
|
||||
mobile_entry::{
|
||||
MobileEntryDraft, MobileEntryEditorError, MobileEntryEditorFieldKind,
|
||||
MobileEntryEditorInput, MobileEntryEditorPage, MobileEntryEditorSession, MobileEntryPage,
|
||||
MobileEntryValueError, field_value,
|
||||
MobileEntryValueError, document_revision, field_value,
|
||||
},
|
||||
mobile_mutation::{
|
||||
MobileMutationAction, MobileMutationError, MobileMutationOutcome, MobileMutationPlan,
|
||||
@@ -949,6 +949,98 @@ impl MobileAuthentication {
|
||||
field_value(&document, field).map_err(value_error)
|
||||
}
|
||||
|
||||
/// Field IDs are scoped to the exact document that the viewer presented.
|
||||
pub fn reveal_presented_entry_field(
|
||||
&self,
|
||||
path: &str,
|
||||
field_id: u64,
|
||||
revision: &str,
|
||||
) -> Result<String, MobileAuthenticationError> {
|
||||
let document = self.open_presented_document(path, revision)?;
|
||||
field_value(&document, field_id).map_err(value_error)
|
||||
}
|
||||
|
||||
pub fn copy_presented_entry_field(
|
||||
&self,
|
||||
path: &str,
|
||||
field_id: u64,
|
||||
revision: &str,
|
||||
) -> Result<MobileEntryCopy, MobileAuthenticationError> {
|
||||
Ok(MobileEntryCopy {
|
||||
value: self.reveal_presented_entry_field(path, field_id, revision)?,
|
||||
timeout_seconds: self.config.clipboard_timeout().duration().as_secs(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn refresh_entry_presentation(
|
||||
&self,
|
||||
path: &str,
|
||||
revision: &str,
|
||||
unix_seconds: u64,
|
||||
) -> Result<MobileEntryPresentation, MobileAuthenticationError> {
|
||||
let presentation = self.entry_page(path, unix_seconds)?;
|
||||
self.check_document_revision(presentation.page().revision(), revision)?;
|
||||
Ok(presentation)
|
||||
}
|
||||
|
||||
fn open_presented_document(
|
||||
&self,
|
||||
path: &str,
|
||||
revision: &str,
|
||||
) -> Result<EntryDocument, MobileAuthenticationError> {
|
||||
let document = self.open_active_document(path)?;
|
||||
self.check_document_revision(&document_revision(&document), revision)?;
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
fn check_document_revision(
|
||||
&self,
|
||||
current: &str,
|
||||
expected: &str,
|
||||
) -> Result<(), MobileAuthenticationError> {
|
||||
if current != expected {
|
||||
self.manual_lock()?;
|
||||
return Err(MobileAuthenticationError::new(
|
||||
MobileAuthenticationErrorKind::Conflict,
|
||||
"Entry Changed",
|
||||
"Refresh and unlock the entry again before using its fields.",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn copy_presented_totp_code(
|
||||
&self,
|
||||
path: &str,
|
||||
revision: &str,
|
||||
unix_seconds: u64,
|
||||
) -> Result<MobileEntryCopy, MobileAuthenticationError> {
|
||||
let presentation = self.refresh_entry_presentation(path, revision, unix_seconds)?;
|
||||
let detail = presentation.totp().ok_or_else(|| {
|
||||
entry_detail(
|
||||
"TOTP Code Is Unavailable",
|
||||
"The entry has no current TOTP code.",
|
||||
)
|
||||
})?;
|
||||
Ok(MobileEntryCopy {
|
||||
value: String::from_utf8(detail.code().expose().to_vec())
|
||||
.map_err(|_| entry_detail("TOTP Code Is Unavailable", "The code is not UTF-8."))?,
|
||||
timeout_seconds: self.config.clipboard_timeout().duration().as_secs(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prepare_presented_entry_mutation(
|
||||
&self,
|
||||
path: &str,
|
||||
revision: &str,
|
||||
action: MobileMutationAction,
|
||||
) -> Result<MobileMutationPlan, MobileAuthenticationError> {
|
||||
self.open_presented_document(path, revision)?;
|
||||
let plan = self.prepare_entry_mutation(path, action)?;
|
||||
self.open_presented_document(path, revision)?;
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
pub fn copy_entry_field(
|
||||
&self,
|
||||
path: &str,
|
||||
@@ -1234,17 +1326,42 @@ impl MobileAuthentication {
|
||||
) -> Result<MobileEntryPage, MobileAuthenticationError> {
|
||||
self.ensure_repository_idle()?;
|
||||
let mut document = self.open_active_document(path)?;
|
||||
self.save_replaced_entry_field(&mut document, field, value)
|
||||
}
|
||||
|
||||
pub fn replace_presented_entry_field(
|
||||
&self,
|
||||
path: &str,
|
||||
field_id: u64,
|
||||
revision: &str,
|
||||
value: String,
|
||||
) -> Result<MobileEntryPage, MobileAuthenticationError> {
|
||||
self.ensure_repository_idle()?;
|
||||
let mut document = self.open_presented_document(path, revision)?;
|
||||
self.save_replaced_entry_field(&mut document, field_id, value)
|
||||
}
|
||||
|
||||
fn save_replaced_entry_field(
|
||||
&self,
|
||||
document: &mut EntryDocument,
|
||||
field_id: u64,
|
||||
value: String,
|
||||
) -> Result<MobileEntryPage, MobileAuthenticationError> {
|
||||
document
|
||||
.replace_field_value(EntryFieldId::from_value(field), value.into_bytes())
|
||||
.replace_field_value(EntryFieldId::from_value(field_id), value.into_bytes())
|
||||
.map_err(document_error)?;
|
||||
let identity = self.git_identity()?;
|
||||
let mut committer = AutomaticEntryCommitter::for_entry(&self.repository, path, identity)
|
||||
.map_err(|error| entry_detail("Password Entry Could Not Be Saved", error))?;
|
||||
let mut committer = AutomaticEntryCommitter::for_entry(
|
||||
&self.repository,
|
||||
&document.path().to_string(),
|
||||
identity,
|
||||
)
|
||||
.map_err(|error| entry_detail("Password Entry Could Not Be Saved", error))?;
|
||||
EntryDocumentService::new(&self.repository, &self.keys)
|
||||
.save_recoverable(&document, None, &mut committer)
|
||||
.save_recoverable(document, None, &mut committer)
|
||||
.map_err(document_error)?;
|
||||
self.reconcile_saved_document(&document);
|
||||
Ok(MobileEntryPage::from_document(&document))
|
||||
self.reconcile_saved_document(document);
|
||||
Ok(MobileEntryPage::from_document(document))
|
||||
}
|
||||
|
||||
pub fn begin_entry_editor(
|
||||
|
||||
Reference in new issue
Block a user