diff --git a/crates/ely_app/src/shell/chrome/split_pane.rs b/crates/ely_app/src/shell/chrome/split_pane.rs index b2f4c21..dd93f37 100644 --- a/crates/ely_app/src/shell/chrome/split_pane.rs +++ b/crates/ely_app/src/shell/chrome/split_pane.rs @@ -96,8 +96,7 @@ pub(crate) fn pane_host_label(tab: &BrowserTab) -> String { } pub(crate) fn pane_url_is_secure(tab: &BrowserTab) -> bool { - let url = tab.url().as_str(); - url.starts_with("https://") || url.starts_with("ely://") + tab.url().has_any_scheme(&["https"]) || tab.url().as_str().starts_with("ely://") } pub(crate) fn split_canvas_status(tab: &BrowserTab) -> String { @@ -144,3 +143,34 @@ pub(crate) fn render_compact_split_canvas(tab: &BrowserTab) -> AnyElement { ) .into_any_element() } + +#[cfg(test)] +mod tests { + use ely_domain::{BrowserTab, ProfileId, SpaceId, TabId, UrlText}; + + use super::pane_url_is_secure; + + #[test] + fn secure_indicator_preserves_web_and_internal_scheme_semantics() + -> Result<(), Box> { + for (url, expected) in [ + ("https://example.com", true), + ("HTTPS://Example.com", true), + ("http://example.com", false), + ("HTTP://Example.com", false), + ("custom://example.com", false), + ("ely://settings", true), + ("ELY://settings", false), + ] { + let tab = BrowserTab::new( + TabId::new(), + SpaceId::new(), + ProfileId::new(), + "Web", + UrlText::parse(url)?, + ); + assert_eq!(pane_url_is_secure(&tab), expected, "{url}"); + } + Ok(()) + } +} diff --git a/crates/ely_app/src/shell/chrome/topbar.rs b/crates/ely_app/src/shell/chrome/topbar.rs index 4ba16ed..09a7f47 100644 --- a/crates/ely_app/src/shell/chrome/topbar.rs +++ b/crates/ely_app/src/shell/chrome/topbar.rs @@ -81,7 +81,7 @@ fn render_omnibar( let active_url = active_tab.url().as_str().to_string(); let command_focused = shell.command_input.read(cx).focus_handle(cx).is_focused(window); let show_styled = !command_focused && active_url != "ely://new-tab"; - let secure = active_url.starts_with("https://") || active_url.starts_with("ely://"); + let secure = super::pane_url_is_secure(active_tab); let omnibar_motion_target = "omnibar-content"; let omnibar_press_id = shell.chrome_motion_animation_id(omnibar_motion_target); diff --git a/crates/ely_app/src/shell/internal_pages.rs b/crates/ely_app/src/shell/internal_pages.rs index 0f6ac47..17cb127 100644 --- a/crates/ely_app/src/shell/internal_pages.rs +++ b/crates/ely_app/src/shell/internal_pages.rs @@ -145,7 +145,7 @@ impl ElyShell { let content = self.render_sync_page(snapshot, cx); render_settings_shell(snapshot, "ely://settings/sync", content, cx) } - url if super::web_surface::is_external_web_url(url) => { + _ if super::web_surface::is_external_web_url(tab.url()) => { self.render_external_web_canvas(tab, snapshot, bottom_corner_radius, cx) } _ => render_default_page(tab), diff --git a/crates/ely_app/src/shell/web_surface.rs b/crates/ely_app/src/shell/web_surface.rs index 77c31af..4f0d15f 100644 --- a/crates/ely_app/src/shell/web_surface.rs +++ b/crates/ely_app/src/shell/web_surface.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use std::time::{Duration, Instant}; -use ely_domain::{BrowserTab, ProfileId, TabId}; +use ely_domain::{BrowserTab, ProfileId, TabId, UrlText}; use crate::services::{ProfileDataMode, servo_live::ServoLivePermissionGrant}; @@ -58,7 +58,7 @@ impl WebSurfaceStore { profile_data_mode: ProfileDataMode, permissions: &[WebSurfaceSitePermission], ) -> bool { - if !is_external_web_url(tab.url().as_str()) { + if !is_external_web_url(tab.url()) { return false; } let requested_url = tab.url().as_str().to_string(); @@ -372,8 +372,8 @@ impl WebSurfaceStore { } } -pub(super) fn is_external_web_url(url: &str) -> bool { - url.starts_with("https://") || url.starts_with("http://") +pub(super) fn is_external_web_url(url: &UrlText) -> bool { + url.has_any_scheme(&["http", "https"]) } #[cfg(test)] diff --git a/crates/ely_app/src/shell/web_surface_controller.rs b/crates/ely_app/src/shell/web_surface_controller.rs index 53336e5..ee36975 100644 --- a/crates/ely_app/src/shell/web_surface_controller.rs +++ b/crates/ely_app/src/shell/web_surface_controller.rs @@ -283,7 +283,7 @@ fn visible_web_surface_tabs( let mut permission_cache = HashMap::new(); let mut visible = Vec::new(); for tab in tabs { - if !super::web_surface::is_external_web_url(tab.url().as_str()) { + if !super::web_surface::is_external_web_url(tab.url()) { continue; } let Ok(kind) = core.profile_kind_for(tab.profile_id()) else { @@ -308,7 +308,7 @@ fn visible_web_surface_tabs( fn external_web_surface_tab_ids(tabs: &[BrowserTab]) -> Vec { tabs.iter() - .filter(|tab| super::web_surface::is_external_web_url(tab.url().as_str())) + .filter(|tab| super::web_surface::is_external_web_url(tab.url())) .map(|tab| tab.id().clone()) .collect() } @@ -316,7 +316,7 @@ fn external_web_surface_tab_ids(tabs: &[BrowserTab]) -> Vec { fn external_web_surface_scopes(core: &BrowserCore) -> Vec<(TabId, ProfileId, ProfileDataMode)> { core.open_tabs() .iter() - .filter(|tab| super::web_surface::is_external_web_url(tab.url().as_str())) + .filter(|tab| super::web_surface::is_external_web_url(tab.url())) .filter_map(|tab| { core.profile_kind_for(tab.profile_id()).ok().map(|kind| { (tab.id().clone(), tab.profile_id().clone(), profile_data_mode_from_kind(kind)) diff --git a/crates/ely_app/src/shell/web_surface_controller_tests.rs b/crates/ely_app/src/shell/web_surface_controller_tests.rs index a1bc3f2..96a7077 100644 --- a/crates/ely_app/src/shell/web_surface_controller_tests.rs +++ b/crates/ely_app/src/shell/web_surface_controller_tests.rs @@ -123,6 +123,20 @@ fn external_tab_ids_exclude_internal_routes() -> Result<(), Box Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + let https_tab_id = core.snapshot()?.active_tab_id; + core.navigate_active_tab(UrlText::parse("HTTPS://Example.com/Path")?)?; + assert_eq!(core.active_tab()?.url().as_str(), "HTTPS://Example.com/Path"); + + let http_tab_id = core.open_tab(UrlText::parse("HTTP://Example.com/Plain")?); + + assert_eq!(core.active_tab()?.url().as_str(), "HTTP://Example.com/Plain"); + assert_eq!(external_web_surface_tab_ids(core.open_tabs()), vec![https_tab_id, http_tab_id]); + Ok(()) +} + #[test] fn external_tab_ids_include_inactive_spaces() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; diff --git a/crates/ely_app/src/shell/web_surface_keyboard.rs b/crates/ely_app/src/shell/web_surface_keyboard.rs index 64b44c0..e06bc42 100644 --- a/crates/ely_app/src/shell/web_surface_keyboard.rs +++ b/crates/ely_app/src/shell/web_surface_keyboard.rs @@ -46,10 +46,10 @@ impl ElyShell { return None; }; let tab = core.active_tab().ok()?; - let requested_url = tab.url().as_str(); - if !is_external_web_url(requested_url) { + if !is_external_web_url(tab.url()) { return None; } + let requested_url = tab.url().as_str(); Some((tab.id().clone(), requested_url.to_string())) } diff --git a/crates/ely_domain/src/url_text.rs b/crates/ely_domain/src/url_text.rs index 9055130..5184ff8 100644 --- a/crates/ely_domain/src/url_text.rs +++ b/crates/ely_domain/src/url_text.rs @@ -44,6 +44,14 @@ impl UrlText { &self.value } + #[must_use] + pub fn has_any_scheme(&self, schemes: &[&str]) -> bool { + let Some((scheme, _)) = self.value.split_once(':') else { + return false; + }; + schemes.iter().any(|candidate| scheme.eq_ignore_ascii_case(candidate)) + } + #[must_use] pub fn display_host(&self) -> String { Url::parse(&self.value) @@ -91,3 +99,18 @@ impl fmt::Display for UrlText { f.write_str(&self.value) } } + +#[cfg(test)] +mod tests { + use super::UrlText; + + #[test] + fn parsed_scheme_matching_preserves_original_text() -> Result<(), Box> { + let url = UrlText::parse("HTTPS://Example.com/Path")?; + + assert!(url.has_any_scheme(&["http", "https"])); + assert!(!url.has_any_scheme(&["custom"])); + assert_eq!(url.as_str(), "HTTPS://Example.com/Path"); + Ok(()) + } +}