Confirm dangerous downloads
This commit is contained in:
@@ -58,7 +58,7 @@ impl DownloadFileAction {
|
|||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
fn label(self) -> &'static str {
|
pub fn label(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Open => "open",
|
Self::Open => "open",
|
||||||
Self::Reveal => "reveal",
|
Self::Reveal => "reveal",
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
use ely_domain::DownloadId;
|
||||||
|
use gpui::Context;
|
||||||
|
|
||||||
|
use crate::services::{
|
||||||
|
download_checksums::DownloadChecksumCalculator, download_files::DownloadFileAction,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{ElyShell, ShellState};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(super) struct PendingDownloadFileAction {
|
||||||
|
download_id: DownloadId,
|
||||||
|
file_name: String,
|
||||||
|
action: DownloadFileAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PendingDownloadFileAction {
|
||||||
|
fn new(download_id: DownloadId, file_name: String, action: DownloadFileAction) -> Self {
|
||||||
|
Self { download_id, file_name, action }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn file_name(&self) -> &str {
|
||||||
|
&self.file_name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn action_label(&self) -> &'static str {
|
||||||
|
self.action.label()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ElyShell {
|
||||||
|
pub(super) fn pause_download(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
||||||
|
if let ShellState::Ready(core) = &mut self.state
|
||||||
|
&& core.pause_download(download_id).is_ok()
|
||||||
|
{
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn resume_download(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
||||||
|
if let ShellState::Ready(core) = &mut self.state
|
||||||
|
&& core.resume_download(download_id).is_ok()
|
||||||
|
{
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn cancel_download(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
||||||
|
if let ShellState::Ready(core) = &mut self.state
|
||||||
|
&& core.cancel_download(download_id).is_ok()
|
||||||
|
{
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn retry_download(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
||||||
|
if let ShellState::Ready(core) = &mut self.state
|
||||||
|
&& core.retry_download(download_id).is_ok()
|
||||||
|
{
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn request_clear_active_profile_downloads(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.download_clear_confirmation = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn cancel_clear_active_profile_downloads(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.download_clear_confirmation = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clear_active_profile_downloads(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if let ShellState::Ready(core) = &mut self.state {
|
||||||
|
core.clear_downloads_for_active_profile();
|
||||||
|
}
|
||||||
|
self.download_clear_confirmation = false;
|
||||||
|
self.download_action_error = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn calculate_download_checksum(
|
||||||
|
&mut self,
|
||||||
|
download_id: &DownloadId,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let result = match &mut self.state {
|
||||||
|
ShellState::Ready(core) => core
|
||||||
|
.download_target_file_path(download_id)
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
.and_then(|path| {
|
||||||
|
DownloadChecksumCalculator::sha256(&path).map_err(|error| error.to_string())
|
||||||
|
})
|
||||||
|
.and_then(|checksum| {
|
||||||
|
core.record_download_checksum(download_id, checksum)
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}),
|
||||||
|
ShellState::StartupError(message) => Err(message.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.download_action_error = result.err();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn open_download_file(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
||||||
|
self.run_download_file_action(download_id, DownloadFileAction::Open, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reveal_download_file(
|
||||||
|
&mut self,
|
||||||
|
download_id: &DownloadId,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.run_download_file_action(download_id, DownloadFileAction::Reveal, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn confirm_download_security_prompt(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let Some(pending_action) = self.download_security_confirmation.take() else {
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match &mut self.state {
|
||||||
|
ShellState::Ready(core) => core
|
||||||
|
.confirm_download_security_prompt(&pending_action.download_id)
|
||||||
|
.map_err(|error| error.to_string()),
|
||||||
|
ShellState::StartupError(message) => Err(message.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(message) = result {
|
||||||
|
self.download_action_error = Some(message);
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.execute_download_file_action(&pending_action.download_id, pending_action.action, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn cancel_download_security_prompt(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.download_security_confirmation = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_download_file_action(
|
||||||
|
&mut self,
|
||||||
|
download_id: &DownloadId,
|
||||||
|
action: DownloadFileAction,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
match self.queue_security_confirmation(download_id, action) {
|
||||||
|
Ok(true) => {
|
||||||
|
self.download_action_error = None;
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(false) => {}
|
||||||
|
Err(message) => {
|
||||||
|
self.download_action_error = Some(message);
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.execute_download_file_action(download_id, action, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue_security_confirmation(
|
||||||
|
&mut self,
|
||||||
|
download_id: &DownloadId,
|
||||||
|
action: DownloadFileAction,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let core = match &self.state {
|
||||||
|
ShellState::Ready(core) => core,
|
||||||
|
ShellState::StartupError(message) => return Err(message.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !core
|
||||||
|
.download_requires_security_confirmation(download_id)
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
{
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name = core
|
||||||
|
.snapshot()
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.download_entries
|
||||||
|
.into_iter()
|
||||||
|
.find(|entry| entry.id() == download_id)
|
||||||
|
.map(|entry| entry.file_name().to_string())
|
||||||
|
.ok_or_else(|| format!("download not found: {download_id}"))?;
|
||||||
|
|
||||||
|
self.download_security_confirmation =
|
||||||
|
Some(PendingDownloadFileAction::new(download_id.clone(), file_name, action));
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_download_file_action(
|
||||||
|
&mut self,
|
||||||
|
download_id: &DownloadId,
|
||||||
|
action: DownloadFileAction,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let result = match &self.state {
|
||||||
|
ShellState::Ready(core) => core
|
||||||
|
.download_target_file_path(download_id)
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
.and_then(|path| action.run(&path).map_err(|error| error.to_string())),
|
||||||
|
ShellState::StartupError(message) => Err(message.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.download_action_error = result.err();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
use ely_domain::{
|
use ely_domain::{DownloadDestination, DownloadEntry, DownloadPolicy, DownloadState};
|
||||||
DownloadDestination, DownloadEntry, DownloadPolicy, DownloadSecurity, DownloadState,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) fn download_state_label(state: &DownloadState) -> &'static str {
|
pub(crate) fn download_state_label(state: &DownloadState) -> &'static str {
|
||||||
match state {
|
match state {
|
||||||
@@ -32,11 +30,11 @@ pub(crate) fn download_entry_location_label(entry: &DownloadEntry) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn download_security_label(security: &DownloadSecurity) -> &'static str {
|
pub(crate) fn download_security_label(entry: &DownloadEntry) -> &'static str {
|
||||||
match security {
|
if entry.requires_security_confirmation() {
|
||||||
DownloadSecurity::Standard => "Standard",
|
return "Confirm before opening";
|
||||||
DownloadSecurity::DangerousExtension => "Extension prompt required",
|
|
||||||
}
|
}
|
||||||
|
"Extension confirmed"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_destination_label(destination: &DownloadDestination) -> String {
|
fn download_destination_label(destination: &DownloadDestination) -> String {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use ely_browser_core::BrowserSnapshot;
|
use ely_browser_core::BrowserSnapshot;
|
||||||
use ely_design_system::colors;
|
use ely_design_system::colors;
|
||||||
use ely_domain::{DownloadChecksum, DownloadEntry, DownloadSecurity};
|
use ely_domain::{DownloadChecksum, DownloadEntry};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div,
|
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div,
|
||||||
@@ -12,6 +12,7 @@ use gpui_component::{
|
|||||||
scroll::ScrollableElement,
|
scroll::ScrollableElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::super::PendingDownloadFileAction;
|
||||||
use super::{
|
use super::{
|
||||||
ElyShell,
|
ElyShell,
|
||||||
download_labels::{
|
download_labels::{
|
||||||
@@ -113,6 +114,9 @@ impl ElyShell {
|
|||||||
self.download_clear_confirmation && !snapshot.download_entries.is_empty(),
|
self.download_clear_confirmation && !snapshot.download_entries.is_empty(),
|
||||||
|this| this.child(self.render_download_clear_confirmation(snapshot, cx)),
|
|this| this.child(self.render_download_clear_confirmation(snapshot, cx)),
|
||||||
)
|
)
|
||||||
|
.when_some(self.download_security_confirmation.clone(), |this, pending_action| {
|
||||||
|
this.child(self.render_download_security_confirmation(&pending_action, cx))
|
||||||
|
})
|
||||||
.child(self.render_downloads_list(snapshot, cx)),
|
.child(self.render_downloads_list(snapshot, cx)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -166,6 +170,63 @@ impl ElyShell {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_download_security_confirmation(
|
||||||
|
&mut self,
|
||||||
|
pending_action: &PendingDownloadFileAction,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.rounded_md()
|
||||||
|
.border_1()
|
||||||
|
.border_color(rgb(colors::ERROR))
|
||||||
|
.px_3()
|
||||||
|
.py_2()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.gap_3()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(rgb(colors::ERROR))
|
||||||
|
.child(IconName::TriangleAlert)
|
||||||
|
.child(div().truncate().child(format!(
|
||||||
|
"Confirm {} for {}",
|
||||||
|
pending_action.action_label(),
|
||||||
|
pending_action.file_name()
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Button::new("cancel-security-download")
|
||||||
|
.ghost()
|
||||||
|
.xsmall()
|
||||||
|
.label("Cancel")
|
||||||
|
.on_click(cx.listener(|shell, _, _, cx| {
|
||||||
|
shell.cancel_download_security_prompt(cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("confirm-security-download")
|
||||||
|
.danger()
|
||||||
|
.xsmall()
|
||||||
|
.label("Confirm")
|
||||||
|
.on_click(cx.listener(|shell, _, _, cx| {
|
||||||
|
shell.confirm_download_security_prompt(cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_downloads_list(
|
fn render_downloads_list(
|
||||||
&mut self,
|
&mut self,
|
||||||
snapshot: &BrowserSnapshot,
|
snapshot: &BrowserSnapshot,
|
||||||
@@ -258,7 +319,7 @@ impl ElyShell {
|
|||||||
.child(download_entry_location_label(entry)),
|
.child(download_entry_location_label(entry)),
|
||||||
)
|
)
|
||||||
.when(entry.security().requires_prompt(), |this| {
|
.when(entry.security().requires_prompt(), |this| {
|
||||||
this.child(render_security_prompt(entry.security()))
|
this.child(render_security_prompt(entry))
|
||||||
})
|
})
|
||||||
.when_some(entry.checksum(), |this, checksum| {
|
.when_some(entry.checksum(), |this, checksum| {
|
||||||
this.child(render_checksum_label(checksum))
|
this.child(render_checksum_label(checksum))
|
||||||
@@ -316,14 +377,22 @@ fn render_download_action_error(message: String) -> AnyElement {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_security_prompt(security: &DownloadSecurity) -> AnyElement {
|
fn render_security_prompt(entry: &DownloadEntry) -> AnyElement {
|
||||||
|
let prompt_color =
|
||||||
|
if entry.requires_security_confirmation() { colors::ERROR } else { colors::SUCCESS };
|
||||||
|
let prompt_icon = if entry.requires_security_confirmation() {
|
||||||
|
IconName::TriangleAlert
|
||||||
|
} else {
|
||||||
|
IconName::CircleCheck
|
||||||
|
};
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.text_color(rgb(colors::ERROR))
|
.text_color(rgb(prompt_color))
|
||||||
.child(IconName::TriangleAlert)
|
.child(prompt_icon)
|
||||||
.child(download_security_label(security))
|
.child(download_security_label(entry))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
|
mod downloads;
|
||||||
mod internal_pages;
|
mod internal_pages;
|
||||||
mod render;
|
mod render;
|
||||||
|
|
||||||
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
||||||
use ely_domain::{CommandIntent, DownloadId, SpaceId, TabId, UrlText};
|
use ely_domain::{CommandIntent, SpaceId, TabId, UrlText};
|
||||||
use gpui::{App, AppContext, Context, Entity, FocusHandle, Focusable, Subscription, Window};
|
use gpui::{App, AppContext, Context, Entity, FocusHandle, Focusable, Subscription, Window};
|
||||||
use gpui_component::input::{InputEvent, InputState, SelectAll};
|
use gpui_component::input::{InputEvent, InputState, SelectAll};
|
||||||
|
|
||||||
|
use downloads::PendingDownloadFileAction;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
|
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
|
||||||
OpenSettings, RestoreClosedTab, SelectNextTab, SelectPreviousTab, ToggleFavoriteTab,
|
OpenSettings, RestoreClosedTab, SelectNextTab, SelectPreviousTab, ToggleFavoriteTab,
|
||||||
TogglePinnedTab,
|
TogglePinnedTab,
|
||||||
services::{
|
|
||||||
download_checksums::DownloadChecksumCalculator, download_files::DownloadFileAction,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ShellState {
|
enum ShellState {
|
||||||
@@ -27,6 +27,7 @@ pub struct ElyShell {
|
|||||||
last_intent: Option<CommandIntent>,
|
last_intent: Option<CommandIntent>,
|
||||||
download_action_error: Option<String>,
|
download_action_error: Option<String>,
|
||||||
download_clear_confirmation: bool,
|
download_clear_confirmation: bool,
|
||||||
|
download_security_confirmation: Option<PendingDownloadFileAction>,
|
||||||
_command_subscription: Subscription,
|
_command_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@ impl ElyShell {
|
|||||||
last_intent: None,
|
last_intent: None,
|
||||||
download_action_error: None,
|
download_action_error: None,
|
||||||
download_clear_confirmation: false,
|
download_clear_confirmation: false,
|
||||||
|
download_security_confirmation: None,
|
||||||
_command_subscription: command_subscription,
|
_command_subscription: command_subscription,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,102 +224,6 @@ impl ElyShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pause_download(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
|
||||||
if let ShellState::Ready(core) = &mut self.state
|
|
||||||
&& core.pause_download(download_id).is_ok()
|
|
||||||
{
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resume_download(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
|
||||||
if let ShellState::Ready(core) = &mut self.state
|
|
||||||
&& core.resume_download(download_id).is_ok()
|
|
||||||
{
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel_download(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
|
||||||
if let ShellState::Ready(core) = &mut self.state
|
|
||||||
&& core.cancel_download(download_id).is_ok()
|
|
||||||
{
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn retry_download(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
|
||||||
if let ShellState::Ready(core) = &mut self.state
|
|
||||||
&& core.retry_download(download_id).is_ok()
|
|
||||||
{
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_clear_active_profile_downloads(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.download_clear_confirmation = true;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel_clear_active_profile_downloads(&mut self, cx: &mut Context<Self>) {
|
|
||||||
self.download_clear_confirmation = false;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_active_profile_downloads(&mut self, cx: &mut Context<Self>) {
|
|
||||||
if let ShellState::Ready(core) = &mut self.state {
|
|
||||||
core.clear_downloads_for_active_profile();
|
|
||||||
}
|
|
||||||
self.download_clear_confirmation = false;
|
|
||||||
self.download_action_error = None;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn calculate_download_checksum(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
|
||||||
let result = match &mut self.state {
|
|
||||||
ShellState::Ready(core) => core
|
|
||||||
.download_target_file_path(download_id)
|
|
||||||
.map_err(|error| error.to_string())
|
|
||||||
.and_then(|path| {
|
|
||||||
DownloadChecksumCalculator::sha256(&path).map_err(|error| error.to_string())
|
|
||||||
})
|
|
||||||
.and_then(|checksum| {
|
|
||||||
core.record_download_checksum(download_id, checksum)
|
|
||||||
.map_err(|error| error.to_string())
|
|
||||||
}),
|
|
||||||
ShellState::StartupError(message) => Err(message.clone()),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.download_action_error = result.err();
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_download_file(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
|
||||||
self.run_download_file_action(download_id, DownloadFileAction::Open, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reveal_download_file(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
|
|
||||||
self.run_download_file_action(download_id, DownloadFileAction::Reveal, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_download_file_action(
|
|
||||||
&mut self,
|
|
||||||
download_id: &DownloadId,
|
|
||||||
action: DownloadFileAction,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
let result = match &self.state {
|
|
||||||
ShellState::Ready(core) => core
|
|
||||||
.download_target_file_path(download_id)
|
|
||||||
.map_err(|error| error.to_string())
|
|
||||||
.and_then(|path| action.run(&path).map_err(|error| error.to_string())),
|
|
||||||
ShellState::StartupError(message) => Err(message.clone()),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.download_action_error = result.err();
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_close_current_tab(
|
fn on_close_current_tab(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &CloseCurrentTab,
|
_: &CloseCurrentTab,
|
||||||
|
|||||||
@@ -79,6 +79,21 @@ impl BrowserCore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn confirm_download_security_prompt(
|
||||||
|
&mut self,
|
||||||
|
download_id: &DownloadId,
|
||||||
|
) -> Result<(), CoreError> {
|
||||||
|
self.download_entry_mut(download_id)?.confirm_security_prompt()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn download_requires_security_confirmation(
|
||||||
|
&self,
|
||||||
|
download_id: &DownloadId,
|
||||||
|
) -> Result<bool, CoreError> {
|
||||||
|
Ok(self.visible_download_entry(download_id)?.requires_security_confirmation())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn download_target_file_path(
|
pub fn download_target_file_path(
|
||||||
&self,
|
&self,
|
||||||
download_id: &DownloadId,
|
download_id: &DownloadId,
|
||||||
|
|||||||
@@ -171,6 +171,63 @@ fn records_active_profile_download_policy_on_started_entry() -> Result<(), Box<d
|
|||||||
assert_eq!(entry.destination(), policy.destination());
|
assert_eq!(entry.destination(), policy.destination());
|
||||||
assert_eq!(entry.target_file_path(), Some(Path::new("/tmp/ely-work-downloads/installer.dmg")));
|
assert_eq!(entry.target_file_path(), Some(Path::new("/tmp/ely-work-downloads/installer.dmg")));
|
||||||
assert_eq!(entry.security(), &DownloadSecurity::DangerousExtension);
|
assert_eq!(entry.security(), &DownloadSecurity::DangerousExtension);
|
||||||
|
assert!(entry.requires_security_confirmation());
|
||||||
|
assert!(!entry.security_prompt_confirmed());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn confirms_dangerous_download_security_prompt() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let download_id = core.record_download_started(
|
||||||
|
UrlText::parse("https://example.com/installer.dmg")?,
|
||||||
|
"installer.dmg",
|
||||||
|
Some(4096),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
core.complete_download(&download_id, 4096)?;
|
||||||
|
assert!(core.download_requires_security_confirmation(&download_id)?);
|
||||||
|
core.confirm_download_security_prompt(&download_id)?;
|
||||||
|
assert!(!core.download_requires_security_confirmation(&download_id)?);
|
||||||
|
assert!(active_download(&core)?.security_prompt_confirmed());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_security_prompt_confirmation_before_completion() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let download_id = core.record_download_started(
|
||||||
|
UrlText::parse("https://example.com/installer.dmg")?,
|
||||||
|
"installer.dmg",
|
||||||
|
Some(4096),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let error = match core.confirm_download_security_prompt(&download_id) {
|
||||||
|
Ok(()) => return Err("security prompt confirmation should require completion".into()),
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
CoreError::Domain(DomainError::InvalidDownloadTransition {
|
||||||
|
action: "confirm security prompt",
|
||||||
|
state: "in_progress"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn standard_download_skips_security_confirmation() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let download_id = core.record_download_started(
|
||||||
|
UrlText::parse("https://example.com/report.pdf")?,
|
||||||
|
"report.pdf",
|
||||||
|
Some(2048),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
core.complete_download(&download_id, 2048)?;
|
||||||
|
assert!(!core.download_requires_security_confirmation(&download_id)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pub struct DownloadEntry {
|
|||||||
received_bytes: u64,
|
received_bytes: u64,
|
||||||
total_bytes: Option<u64>,
|
total_bytes: Option<u64>,
|
||||||
checksum: Option<DownloadChecksum>,
|
checksum: Option<DownloadChecksum>,
|
||||||
|
security_prompt_confirmed: bool,
|
||||||
started_at: SystemTime,
|
started_at: SystemTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +213,7 @@ impl DownloadEntry {
|
|||||||
received_bytes: 0,
|
received_bytes: 0,
|
||||||
total_bytes,
|
total_bytes,
|
||||||
checksum: None,
|
checksum: None,
|
||||||
|
security_prompt_confirmed: false,
|
||||||
started_at,
|
started_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -268,6 +270,12 @@ impl DownloadEntry {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn confirm_security_prompt(&mut self) -> Result<(), DomainError> {
|
||||||
|
self.require_state("confirm security prompt", &[DownloadState::Completed])?;
|
||||||
|
self.security_prompt_confirmed = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn id(&self) -> &DownloadId {
|
pub fn id(&self) -> &DownloadId {
|
||||||
&self.id
|
&self.id
|
||||||
@@ -303,6 +311,16 @@ impl DownloadEntry {
|
|||||||
&self.security
|
&self.security
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn requires_security_confirmation(&self) -> bool {
|
||||||
|
self.security.requires_prompt() && !self.security_prompt_confirmed
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn security_prompt_confirmed(&self) -> bool {
|
||||||
|
self.security_prompt_confirmed
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn state(&self) -> &DownloadState {
|
pub fn state(&self) -> &DownloadState {
|
||||||
&self.state
|
&self.state
|
||||||
|
|||||||
Reference in New Issue
Block a user