Add active profile site data clearing
This commit is contained in:
@@ -5,5 +5,5 @@ mod state;
|
|||||||
pub use error::CoreError;
|
pub use error::CoreError;
|
||||||
pub use state::{
|
pub use state::{
|
||||||
BrowserCore, BrowserSnapshot, InitialBrowserConfig, InstalledPlugin, PluginAuditAction,
|
BrowserCore, BrowserSnapshot, InitialBrowserConfig, InstalledPlugin, PluginAuditAction,
|
||||||
PluginAuditEvent, TrashedSpace,
|
PluginAuditEvent, SiteDataClearance, TrashedSpace,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ mod notes;
|
|||||||
mod plugins;
|
mod plugins;
|
||||||
mod profiles;
|
mod profiles;
|
||||||
mod reading_list;
|
mod reading_list;
|
||||||
|
mod site_data;
|
||||||
mod site_permissions;
|
mod site_permissions;
|
||||||
mod spaces;
|
mod spaces;
|
||||||
mod splits;
|
mod splits;
|
||||||
@@ -30,6 +31,7 @@ mod tab_order;
|
|||||||
mod tabs;
|
mod tabs;
|
||||||
|
|
||||||
pub use plugins::{InstalledPlugin, PluginAuditAction, PluginAuditEvent};
|
pub use plugins::{InstalledPlugin, PluginAuditAction, PluginAuditEvent};
|
||||||
|
pub use site_data::SiteDataClearance;
|
||||||
pub use spaces::TrashedSpace;
|
pub use spaces::TrashedSpace;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -285,6 +285,12 @@ impl BrowserCore {
|
|||||||
self.open_tab(url);
|
self.open_tab(url);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
"clear-site-data"
|
||||||
|
| "clear site data"
|
||||||
|
| "clear-site-data-for-this-profile"
|
||||||
|
| "clear site data for this profile" => {
|
||||||
|
Ok(self.clear_active_profile_site_data()?.is_some())
|
||||||
|
}
|
||||||
"about" | "open-about" | "open about" => {
|
"about" | "open-about" | "open about" => {
|
||||||
self.open_tab(about_url()?);
|
self.open_tab(about_url()?);
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
use ely_domain::{HistoryEntry, ProfileId, SiteOrigin, SitePermissionAuditAction};
|
||||||
|
|
||||||
|
use crate::CoreError;
|
||||||
|
|
||||||
|
use super::BrowserCore;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct SiteDataClearance {
|
||||||
|
history_entries: usize,
|
||||||
|
site_permissions: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SiteDataClearance {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(history_entries: usize, site_permissions: usize) -> Self {
|
||||||
|
Self { history_entries, site_permissions }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn history_entries(self) -> usize {
|
||||||
|
self.history_entries
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn site_permissions(self) -> usize {
|
||||||
|
self.site_permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn total_items(self) -> usize {
|
||||||
|
self.history_entries + self.site_permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrowserCore {
|
||||||
|
pub fn clear_active_profile_site_data(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Option<SiteDataClearance>, CoreError> {
|
||||||
|
let Some(origin) = SiteOrigin::from_url(self.active_tab()?.url())? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let profile_id = self.active_profile_id.clone();
|
||||||
|
let history_entries = self.clear_profile_history_for_origin(&profile_id, &origin)?;
|
||||||
|
let site_permissions = self.clear_site_permissions_for_origin(&profile_id, &origin)?;
|
||||||
|
|
||||||
|
Ok(Some(SiteDataClearance::new(history_entries, site_permissions)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_profile_history_for_origin(
|
||||||
|
&mut self,
|
||||||
|
profile_id: &ProfileId,
|
||||||
|
origin: &SiteOrigin,
|
||||||
|
) -> Result<usize, CoreError> {
|
||||||
|
self.require_site_data_profile(profile_id)?;
|
||||||
|
let original_count = self.history_entries.len();
|
||||||
|
self.history_entries.retain(|entry| {
|
||||||
|
entry.profile_id() != profile_id || !history_entry_matches_origin(entry, origin)
|
||||||
|
});
|
||||||
|
Ok(original_count - self.history_entries.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_site_permissions_for_origin(
|
||||||
|
&mut self,
|
||||||
|
profile_id: &ProfileId,
|
||||||
|
origin: &SiteOrigin,
|
||||||
|
) -> Result<usize, CoreError> {
|
||||||
|
self.require_site_data_profile(profile_id)?;
|
||||||
|
let mut revoked_permissions = Vec::new();
|
||||||
|
|
||||||
|
self.site_permissions.retain(|entry| {
|
||||||
|
if entry.profile_id() == profile_id && entry.origin() == origin {
|
||||||
|
revoked_permissions.push((entry.origin().clone(), entry.feature()));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let revoked_count = revoked_permissions.len();
|
||||||
|
for (origin, feature) in revoked_permissions {
|
||||||
|
self.record_site_permission_audit_event(
|
||||||
|
profile_id.clone(),
|
||||||
|
origin,
|
||||||
|
feature,
|
||||||
|
SitePermissionAuditAction::Revoked,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(revoked_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_site_data_profile(&self, profile_id: &ProfileId) -> Result<(), CoreError> {
|
||||||
|
if self.profiles.iter().any(|profile| profile.id() == profile_id) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(CoreError::ProfileNotFound { id: profile_id.clone() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn history_entry_matches_origin(entry: &HistoryEntry, origin: &SiteOrigin) -> bool {
|
||||||
|
SiteOrigin::from_url(entry.url()).ok().flatten().as_ref() == Some(origin)
|
||||||
|
}
|
||||||
@@ -174,7 +174,7 @@ impl BrowserCore {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn record_site_permission_audit_event(
|
pub(super) fn record_site_permission_audit_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
profile_id: ProfileId,
|
profile_id: ProfileId,
|
||||||
origin: SiteOrigin,
|
origin: SiteOrigin,
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
||||||
|
use ely_domain::{
|
||||||
|
CommandIntent, ProfileKind, SiteOrigin, SitePermissionDecision, SitePermissionFeature, UrlText,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_site_data_command_scopes_to_active_origin_and_profile() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let default_profile_id = core.snapshot()?.active_profile_id;
|
||||||
|
let work_space_id = core.snapshot()?.active_space_id;
|
||||||
|
let example_origin = SiteOrigin::parse("https://example.com")?;
|
||||||
|
let other_origin = SiteOrigin::parse("https://example.org")?;
|
||||||
|
|
||||||
|
let example_tab_id = core.open_tab(UrlText::parse("https://example.com/work")?);
|
||||||
|
core.open_tab(UrlText::parse("https://example.org/work")?);
|
||||||
|
core.create_space("Research", "R", 0xf54e00)?;
|
||||||
|
core.open_tab(UrlText::parse("https://example.com/research")?);
|
||||||
|
core.set_site_permission(
|
||||||
|
example_origin.clone(),
|
||||||
|
SitePermissionFeature::Camera,
|
||||||
|
SitePermissionDecision::AllowAlways,
|
||||||
|
)?;
|
||||||
|
core.set_site_permission(
|
||||||
|
other_origin,
|
||||||
|
SitePermissionFeature::Notifications,
|
||||||
|
SitePermissionDecision::DenyAlways,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let personal_profile_id = core.create_profile("Personal", 0xf54e00, ProfileKind::Standard)?;
|
||||||
|
core.select_profile(&personal_profile_id)?;
|
||||||
|
core.open_tab(UrlText::parse("https://example.com/personal")?);
|
||||||
|
core.set_site_permission(
|
||||||
|
example_origin,
|
||||||
|
SitePermissionFeature::Microphone,
|
||||||
|
SitePermissionDecision::DenyAlways,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
core.select_profile(&default_profile_id)?;
|
||||||
|
core.select_space(&work_space_id)?;
|
||||||
|
core.select_tab(&example_tab_id)?;
|
||||||
|
core.set_command_query(">clear-site-data-for-this-profile");
|
||||||
|
let intent = core.submit_command()?;
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
intent,
|
||||||
|
Some(CommandIntent::Command("clear-site-data-for-this-profile".to_string()))
|
||||||
|
);
|
||||||
|
assert_eq!(snapshot.command_query, "");
|
||||||
|
assert_eq!(snapshot.history_entries.len(), 1);
|
||||||
|
assert_eq!(snapshot.active_profile_history_entry_count, 1);
|
||||||
|
assert_eq!(snapshot.history_entries[0].url().as_str(), "https://example.org/work");
|
||||||
|
assert_eq!(snapshot.site_permissions.len(), 1);
|
||||||
|
assert_eq!(snapshot.site_permissions[0].origin().as_str(), "https://example.org");
|
||||||
|
assert_eq!(snapshot.site_permission_audit_events.len(), 3);
|
||||||
|
|
||||||
|
core.select_profile(&personal_profile_id)?;
|
||||||
|
let personal_snapshot = core.snapshot()?;
|
||||||
|
assert_eq!(personal_snapshot.active_profile_history_entry_count, 1);
|
||||||
|
assert_eq!(personal_snapshot.site_permissions.len(), 1);
|
||||||
|
assert_eq!(personal_snapshot.site_permissions[0].origin().as_str(), "https://example.com");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_site_data_command_preserves_query_for_internal_page() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|
||||||
|
core.set_command_query(">clear-site-data-for-this-profile");
|
||||||
|
let intent = core.submit_command()?;
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
intent,
|
||||||
|
Some(CommandIntent::Command("clear-site-data-for-this-profile".to_string()))
|
||||||
|
);
|
||||||
|
assert_eq!(snapshot.command_query, ">clear-site-data-for-this-profile");
|
||||||
|
assert!(snapshot.history_entries.is_empty());
|
||||||
|
assert!(snapshot.site_permissions.is_empty());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clear_active_profile_site_data_reports_removed_counts() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let origin = SiteOrigin::parse("https://example.com")?;
|
||||||
|
|
||||||
|
core.open_tab(UrlText::parse("https://example.com/account")?);
|
||||||
|
core.set_site_permission(
|
||||||
|
origin.clone(),
|
||||||
|
SitePermissionFeature::Camera,
|
||||||
|
SitePermissionDecision::AllowAlways,
|
||||||
|
)?;
|
||||||
|
core.set_site_permission(
|
||||||
|
origin,
|
||||||
|
SitePermissionFeature::Notifications,
|
||||||
|
SitePermissionDecision::DenyAlways,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let clearance =
|
||||||
|
core.clear_active_profile_site_data()?.ok_or("missing active site data clearance")?;
|
||||||
|
|
||||||
|
assert_eq!(clearance.history_entries(), 1);
|
||||||
|
assert_eq!(clearance.site_permissions(), 2);
|
||||||
|
assert_eq!(clearance.total_items(), 3);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user