fix(permissions): make profile snapshots authoritative
This commit is contained in:
@@ -74,6 +74,8 @@ pub(super) fn run(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
||||
request,
|
||||
)
|
||||
});
|
||||
let outcome = outcome
|
||||
.map(|outcome| outcome.with_permission_consumptions(host.take_consumed_permissions()));
|
||||
write_outcome(&mut stdout, outcome)?;
|
||||
if should_shutdown {
|
||||
break;
|
||||
@@ -126,6 +128,7 @@ fn handle_request(
|
||||
hover_x,
|
||||
hover_y,
|
||||
typed_text,
|
||||
site_permission_generation,
|
||||
site_permissions,
|
||||
ready_surface_ids,
|
||||
pending_surface_ids,
|
||||
@@ -138,7 +141,13 @@ fn handle_request(
|
||||
let session =
|
||||
ensure_session(host, sessions, tab_id.clone(), &tab, &profile, width, height)?;
|
||||
apply_layout(host, session, width, height, page_zoom_percent, device_pixel_ratio)?;
|
||||
apply_permissions(host, session, &profile, site_permissions)?;
|
||||
apply_permissions(
|
||||
host,
|
||||
session,
|
||||
&profile,
|
||||
site_permission_generation,
|
||||
site_permissions,
|
||||
)?;
|
||||
if session.requested_url != url.as_str() {
|
||||
let servo_current_url =
|
||||
host.snapshot(&session.webview_id)?.url().map(str::to_string);
|
||||
|
||||
@@ -13,7 +13,9 @@ pub(super) fn write_outcome(
|
||||
if let Some(frame) = outcome.frame.as_ref()
|
||||
&& let Err(error) = validate_frame(frame.width(), frame.height(), frame.rgba_bytes().len())
|
||||
{
|
||||
let consumptions = std::mem::take(&mut outcome.response.permission_consumptions);
|
||||
outcome = LiveOutcome::error(error.to_string());
|
||||
outcome.response.permission_consumptions = consumptions;
|
||||
}
|
||||
|
||||
serde_json::to_writer(&mut *stdout, &outcome.response)?;
|
||||
@@ -39,11 +41,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mismatched_frame_becomes_header_only_error() -> Result<(), LiveSidecarError> {
|
||||
let profile_id = ely_domain::ProfileId::new();
|
||||
let frame = ely_servo_host::RenderedFrame::from_rgba_bytes(2, 2, vec![0; 4]);
|
||||
let snapshot = ely_servo_host::WebViewSnapshot::new(
|
||||
ely_domain::WebViewId::new(),
|
||||
ely_domain::TabId::new(),
|
||||
ely_domain::ProfileId::new(),
|
||||
profile_id.clone(),
|
||||
ely_servo_host::WebViewState::Complete,
|
||||
None,
|
||||
None,
|
||||
@@ -52,13 +55,23 @@ mod tests {
|
||||
let report =
|
||||
super::super::live_protocol::LiveFrameReport::new(&snapshot, &frame, 1.0, true);
|
||||
let mut output = Vec::new();
|
||||
let outcome = LiveOutcome::frame(report, frame).with_permission_consumptions(vec![
|
||||
ely_servo_host::ConsumedPermission {
|
||||
profile_id: profile_id.clone(),
|
||||
origin: ely_domain::SiteOrigin::parse("https://example.com")?,
|
||||
feature: ely_domain::SitePermissionFeature::Camera,
|
||||
grant_revision: 7,
|
||||
},
|
||||
]);
|
||||
|
||||
write_outcome(&mut output, Ok(LiveOutcome::frame(report, frame)))?;
|
||||
write_outcome(&mut output, Ok(outcome))?;
|
||||
|
||||
assert!(output.ends_with(b"\n"));
|
||||
let response: serde_json::Value = serde_json::from_slice(&output)?;
|
||||
assert!(response["error"].as_str().is_some());
|
||||
assert!(response["frame"].is_null());
|
||||
assert_eq!(response["permission_consumptions"][0]["profile_id"], profile_id.as_str());
|
||||
assert_eq!(response["permission_consumptions"][0]["grant_revision"], 7);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::io;
|
||||
|
||||
use ely_servo_host::{
|
||||
IOSurfaceHandle, RenderedFrame, ServoHostError, WebViewSnapshot, WebViewState,
|
||||
ConsumedPermission, IOSurfaceHandle, RenderedFrame, ServoHostError, WebViewSnapshot,
|
||||
WebViewState,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
@@ -9,7 +10,7 @@ use thiserror::Error;
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
use super::iosurface_mach::IOSurfaceMachError;
|
||||
|
||||
pub(super) const LIVE_PROTOCOL_VERSION: u32 = 2;
|
||||
pub(super) const LIVE_PROTOCOL_VERSION: u32 = 3;
|
||||
pub(super) const MAX_FRAME_DIMENSION: u32 = 16_384;
|
||||
pub(super) const MAX_FRAME_BYTE_COUNT: usize = 256 * 1024 * 1024;
|
||||
|
||||
@@ -47,6 +48,7 @@ pub(super) enum LiveRequest {
|
||||
hover_y: Option<u32>,
|
||||
#[serde(default)]
|
||||
typed_text: Option<String>,
|
||||
site_permission_generation: u64,
|
||||
#[serde(default)]
|
||||
site_permissions: Vec<LiveSitePermission>,
|
||||
#[serde(default)]
|
||||
@@ -79,7 +81,8 @@ const fn default_device_pixel_ratio() -> f32 {
|
||||
pub(super) struct LiveSitePermission {
|
||||
pub(super) origin: String,
|
||||
pub(super) feature: String,
|
||||
pub(super) decision: String,
|
||||
pub(super) state: String,
|
||||
pub(super) revision: u64,
|
||||
}
|
||||
|
||||
pub(super) struct LiveOutcome {
|
||||
@@ -100,6 +103,15 @@ impl LiveOutcome {
|
||||
Self { response: LiveResponse::frame(report), frame: Some(frame) }
|
||||
}
|
||||
|
||||
pub(super) fn with_permission_consumptions(
|
||||
mut self,
|
||||
consumptions: Vec<ConsumedPermission>,
|
||||
) -> Self {
|
||||
self.response.permission_consumptions =
|
||||
consumptions.into_iter().map(LivePermissionConsumption::from).collect();
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
pub(super) fn surface(report: LiveFrameReport) -> Self {
|
||||
Self { response: LiveResponse::frame(report), frame: None }
|
||||
@@ -115,6 +127,27 @@ pub(super) struct LiveResponse {
|
||||
pub(super) surface_handle: Option<IOSurfaceHandle>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(super) current_surface_id: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub(super) permission_consumptions: Vec<LivePermissionConsumption>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(super) struct LivePermissionConsumption {
|
||||
pub(super) profile_id: String,
|
||||
pub(super) origin: String,
|
||||
pub(super) feature: String,
|
||||
pub(super) grant_revision: u64,
|
||||
}
|
||||
|
||||
impl From<ConsumedPermission> for LivePermissionConsumption {
|
||||
fn from(consumed: ConsumedPermission) -> Self {
|
||||
Self {
|
||||
profile_id: consumed.profile_id.as_str().to_string(),
|
||||
origin: consumed.origin.as_str().to_string(),
|
||||
feature: consumed.feature.as_str().to_string(),
|
||||
grant_revision: consumed.grant_revision,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LiveResponse {
|
||||
@@ -125,6 +158,7 @@ impl LiveResponse {
|
||||
frame: None,
|
||||
surface_handle: None,
|
||||
current_surface_id: None,
|
||||
permission_consumptions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +169,7 @@ impl LiveResponse {
|
||||
frame: None,
|
||||
surface_handle: None,
|
||||
current_surface_id: None,
|
||||
permission_consumptions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +180,7 @@ impl LiveResponse {
|
||||
frame: Some(frame),
|
||||
surface_handle: None,
|
||||
current_surface_id: None,
|
||||
permission_consumptions: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,7 +368,7 @@ mod tests {
|
||||
#[test]
|
||||
fn ensure_defaults_optional_input_fields() -> Result<(), serde_json::Error> {
|
||||
let request = serde_json::from_str::<LiveRequest>(
|
||||
r#"{"type":"ensure","tab_id":"tab","profile_id":"profile","url":"https://example.com","width":800,"height":600}"#,
|
||||
r#"{"type":"ensure","tab_id":"tab","profile_id":"profile","url":"https://example.com","width":800,"height":600,"site_permission_generation":0}"#,
|
||||
)?;
|
||||
|
||||
assert!(matches!(
|
||||
@@ -353,12 +389,21 @@ mod tests {
|
||||
#[test]
|
||||
fn handshake_deserializes_protocol_version() -> Result<(), serde_json::Error> {
|
||||
let request =
|
||||
serde_json::from_str::<LiveRequest>(r#"{"type":"handshake","protocol_version":2}"#)?;
|
||||
serde_json::from_str::<LiveRequest>(r#"{"type":"handshake","protocol_version":3}"#)?;
|
||||
|
||||
assert!(matches!(request, LiveRequest::Handshake { protocol_version: 2 }));
|
||||
assert!(matches!(request, LiveRequest::Handshake { protocol_version: 3 }));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn site_permission_requires_revision() {
|
||||
let request = serde_json::from_str::<LiveRequest>(
|
||||
r#"{"type":"ensure","tab_id":"tab","profile_id":"profile","url":"https://example.com","width":800,"height":600,"site_permission_generation":0,"site_permissions":[{"origin":"https://example.com","feature":"camera","state":"allow-once"}]}"#,
|
||||
);
|
||||
|
||||
assert!(request.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_layout_enforces_dimension_and_byte_limits() {
|
||||
assert!(matches!(
|
||||
|
||||
@@ -3,7 +3,8 @@ use std::collections::{HashMap, hash_map::Entry};
|
||||
use ely_domain::{ProfileId, TabId, validate_zoom_percent};
|
||||
use ely_servo_host::{
|
||||
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseHoverRequest, PageZoomRequest,
|
||||
PermissionDecision, PermissionRequest, RenderedFrame, ResizeRequest, ScrollRequest, ServoHost,
|
||||
PermissionDecision, PermissionSnapshotEntry, PermissionSnapshotRequest,
|
||||
PermissionSnapshotState, RenderedFrame, ResizeRequest, ScrollRequest, ServoHost,
|
||||
ServoSurfaceSize, SoftwareServoHost,
|
||||
};
|
||||
|
||||
@@ -132,22 +133,41 @@ pub(super) fn apply_permissions(
|
||||
host: &mut SoftwareServoHost,
|
||||
session: &LiveSession,
|
||||
profile_id: &ProfileId,
|
||||
generation: u64,
|
||||
permissions: Vec<LiveSitePermission>,
|
||||
) -> Result<(), LiveSidecarError> {
|
||||
for permission in permissions {
|
||||
host.set_permission(
|
||||
PermissionRequest {
|
||||
webview_id: session.webview_id.clone(),
|
||||
profile_id: profile_id.clone(),
|
||||
let entries = permissions
|
||||
.into_iter()
|
||||
.map(|permission| {
|
||||
let state = match permission.state.as_str() {
|
||||
"allow-once" => PermissionSnapshotState::Decision(PermissionDecision::AllowOnce),
|
||||
"allow-always" => {
|
||||
PermissionSnapshotState::Decision(PermissionDecision::AllowAlways)
|
||||
}
|
||||
"deny-always" => PermissionSnapshotState::Decision(PermissionDecision::DenyAlways),
|
||||
"transferred-allow-once" => PermissionSnapshotState::TransferredAllowOnce,
|
||||
value => {
|
||||
return Err(ely_domain::DomainError::InvalidSitePermissionDecision {
|
||||
value: value.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
};
|
||||
Ok(PermissionSnapshotEntry {
|
||||
origin: ely_domain::SiteOrigin::parse(permission.origin)?,
|
||||
feature: ely_domain::SitePermissionFeature::parse(&permission.feature)?,
|
||||
},
|
||||
PermissionDecision::from(ely_domain::SitePermissionDecision::parse(
|
||||
&permission.decision,
|
||||
)?),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
state,
|
||||
revision: permission.revision,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, LiveSidecarError>>()?;
|
||||
host.replace_permissions(PermissionSnapshotRequest {
|
||||
webview_id: session.webview_id.clone(),
|
||||
profile_id: profile_id.clone(),
|
||||
generation,
|
||||
entries,
|
||||
})
|
||||
.map_err(LiveSidecarError::from)
|
||||
}
|
||||
|
||||
pub(super) struct LiveInput {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ely_domain::{ProfileId, WebViewId};
|
||||
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature, WebViewId};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
||||
@@ -15,6 +15,13 @@ pub enum ServoHostError {
|
||||
#[error("permission profile mismatch for {webview_id}: expected {expected}, got {actual}")]
|
||||
PermissionProfileMismatch { webview_id: WebViewId, expected: ProfileId, actual: ProfileId },
|
||||
|
||||
#[error("duplicate permission snapshot entry for {profile_id} {origin:?} {feature:?}")]
|
||||
DuplicatePermissionSnapshotEntry {
|
||||
profile_id: ProfileId,
|
||||
origin: SiteOrigin,
|
||||
feature: SitePermissionFeature,
|
||||
},
|
||||
|
||||
#[error("servo runtime is already started in this process")]
|
||||
RuntimeAlreadyStarted,
|
||||
|
||||
|
||||
@@ -314,21 +314,43 @@ pub struct KeyboardTextRequest {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PermissionRequest {
|
||||
pub webview_id: WebViewId,
|
||||
pub profile_id: ProfileId,
|
||||
pub origin: SiteOrigin,
|
||||
pub feature: SitePermissionFeature,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PermissionDecision {
|
||||
AllowOnce,
|
||||
AllowAlways,
|
||||
DenyAlways,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PermissionSnapshotState {
|
||||
Decision(PermissionDecision),
|
||||
TransferredAllowOnce,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PermissionSnapshotEntry {
|
||||
pub origin: SiteOrigin,
|
||||
pub feature: SitePermissionFeature,
|
||||
pub state: PermissionSnapshotState,
|
||||
pub revision: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PermissionSnapshotRequest {
|
||||
pub webview_id: WebViewId,
|
||||
pub profile_id: ProfileId,
|
||||
pub generation: u64,
|
||||
pub entries: Vec<PermissionSnapshotEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ConsumedPermission {
|
||||
pub profile_id: ProfileId,
|
||||
pub origin: SiteOrigin,
|
||||
pub feature: SitePermissionFeature,
|
||||
pub grant_revision: u64,
|
||||
}
|
||||
|
||||
impl From<SitePermissionDecision> for PermissionDecision {
|
||||
fn from(decision: SitePermissionDecision) -> Self {
|
||||
match decision {
|
||||
@@ -366,12 +388,13 @@ pub trait ServoHost {
|
||||
|
||||
fn type_text(&mut self, request: KeyboardTextRequest) -> Result<(), ServoHostError>;
|
||||
|
||||
fn set_permission(
|
||||
fn replace_permissions(
|
||||
&mut self,
|
||||
request: PermissionRequest,
|
||||
decision: PermissionDecision,
|
||||
request: PermissionSnapshotRequest,
|
||||
) -> Result<(), ServoHostError>;
|
||||
|
||||
fn take_consumed_permissions(&mut self) -> Vec<ConsumedPermission>;
|
||||
|
||||
fn state(&self, webview_id: &WebViewId) -> Result<WebViewState, ServoHostError>;
|
||||
|
||||
fn snapshot(&self, webview_id: &WebViewId) -> Result<WebViewSnapshot, ServoHostError>;
|
||||
|
||||
@@ -20,8 +20,9 @@ pub use error::ServoHostError;
|
||||
#[cfg(feature = "hardware-render")]
|
||||
pub use hardware_rendering_context::HardwareOffscreenContext;
|
||||
pub use host::{
|
||||
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseDragRequest, MouseHoverRequest,
|
||||
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, RenderedFrame,
|
||||
ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest,
|
||||
MouseDragRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest, PermissionDecision,
|
||||
PermissionSnapshotEntry, PermissionSnapshotRequest, PermissionSnapshotState, RenderedFrame,
|
||||
RenderedFrameSummary, ResizeRequest, ScrollRequest, ServoHost, TouchTapRequest,
|
||||
WebViewSnapshot, WebViewSnapshotPending, WebViewState,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
@@ -13,8 +12,8 @@ use dpi::PhysicalSize;
|
||||
use ely_domain::{ProfileId, TabId, WebViewId};
|
||||
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
||||
use servo::{
|
||||
DevicePoint, DeviceVector2D, Opts, Preferences, Scroll, Servo, ServoBuilder, WebViewBuilder,
|
||||
WebViewPoint, WebViewVector,
|
||||
DevicePoint, DeviceVector2D, Opts, Scroll, Servo, ServoBuilder, WebViewBuilder, WebViewPoint,
|
||||
WebViewVector,
|
||||
};
|
||||
|
||||
#[path = "runtime_context.rs"]
|
||||
@@ -24,20 +23,25 @@ mod runtime_context;
|
||||
mod runtime_hardware;
|
||||
#[path = "runtime_paint.rs"]
|
||||
mod runtime_paint;
|
||||
#[path = "runtime_preferences.rs"]
|
||||
mod runtime_preferences;
|
||||
|
||||
use runtime_context::hidpi_scale_from_factor;
|
||||
pub use runtime_context::{RenderingContextKind, ServoSurfaceSize};
|
||||
use runtime_preferences::ely_servo_preferences;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest, MouseDragRequest, MouseHoverRequest,
|
||||
NavigationRequest, PageZoomRequest, PermissionDecision, PermissionRequest, RenderedFrame,
|
||||
ResizeRequest, ScrollRequest, ServoHost, ServoHostError, TouchTapRequest, WebViewSnapshot,
|
||||
WebViewState,
|
||||
ConsumedPermission, HidpiScaleRequest, KeyboardTextRequest, MouseClickRequest,
|
||||
MouseDragRequest, MouseHoverRequest, NavigationRequest, PageZoomRequest,
|
||||
PermissionSnapshotRequest, RenderedFrame, ResizeRequest, ScrollRequest, ServoHost,
|
||||
ServoHostError, TouchTapRequest, WebViewSnapshot, WebViewState,
|
||||
runtime_input::{
|
||||
send_keyboard_text, send_mouse_click, send_mouse_drag, send_mouse_hover, send_touch_tap,
|
||||
},
|
||||
runtime_permissions::{PermissionStore, set_permission_decision},
|
||||
runtime_permissions::{
|
||||
PermissionStore, drain_consumed_permissions, replace_permission_decisions,
|
||||
},
|
||||
runtime_waker::ServoWakeFlag,
|
||||
runtime_webview::{HostWebView, HostWebViewDelegate},
|
||||
};
|
||||
@@ -135,7 +139,7 @@ impl SoftwareServoHost {
|
||||
default_surface_size: size,
|
||||
rendering_context_kind,
|
||||
webviews: HashMap::new(),
|
||||
permissions: Rc::new(RefCell::new(HashMap::new())),
|
||||
permissions: PermissionStore::default(),
|
||||
wake_requested,
|
||||
last_rendered_frame: None,
|
||||
})
|
||||
@@ -166,25 +170,6 @@ fn install_rustls_provider() {
|
||||
});
|
||||
}
|
||||
|
||||
fn ely_servo_preferences() -> Preferences {
|
||||
// `Preferences::default()` is Servo's conservative *library* default: it ships
|
||||
// modern-layout features off even though servo-layout/Stylo implement them and
|
||||
// Servo's own servoshell browser enables them. `Servo::new` forwards these to
|
||||
// Stylo via `prefs::set`, so an embedder building a browser must turn them on or
|
||||
// pages render wrong:
|
||||
// - `layout.grid.enabled` off => Stylo blockifies `display: grid`, collapsing
|
||||
// grid layouts into one stacked column (the "broken" modern-site render).
|
||||
// - `layout.variable_fonts.enabled` off => `font-variation-settings` and
|
||||
// variable weight/width axes are ignored, so a variable font only ever
|
||||
// renders its default instance (every requested weight looks identical).
|
||||
Preferences {
|
||||
dom_intersection_observer_enabled: true,
|
||||
layout_grid_enabled: true,
|
||||
layout_variable_fonts_enabled: true,
|
||||
..Preferences::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl ServoHost for SoftwareServoHost {
|
||||
fn create_webview(
|
||||
&mut self,
|
||||
@@ -332,10 +317,9 @@ impl ServoHost for SoftwareServoHost {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_permission(
|
||||
fn replace_permissions(
|
||||
&mut self,
|
||||
request: PermissionRequest,
|
||||
decision: PermissionDecision,
|
||||
request: PermissionSnapshotRequest,
|
||||
) -> Result<(), ServoHostError> {
|
||||
let webview = self
|
||||
.webviews
|
||||
@@ -348,11 +332,29 @@ impl ServoHost for SoftwareServoHost {
|
||||
actual: request.profile_id,
|
||||
});
|
||||
}
|
||||
|
||||
set_permission_decision(&self.permissions, request, decision);
|
||||
let mut keys = HashSet::new();
|
||||
for entry in &request.entries {
|
||||
if !keys.insert((entry.origin.clone(), entry.feature)) {
|
||||
return Err(ServoHostError::DuplicatePermissionSnapshotEntry {
|
||||
profile_id: request.profile_id,
|
||||
origin: entry.origin.clone(),
|
||||
feature: entry.feature,
|
||||
});
|
||||
}
|
||||
}
|
||||
replace_permission_decisions(
|
||||
&self.permissions,
|
||||
&request.profile_id,
|
||||
request.generation,
|
||||
request.entries,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn take_consumed_permissions(&mut self) -> Vec<ConsumedPermission> {
|
||||
drain_consumed_permissions(&self.permissions)
|
||||
}
|
||||
|
||||
fn state(&self, webview_id: &WebViewId) -> Result<WebViewState, ServoHostError> {
|
||||
Ok(self.webview(webview_id)?.state())
|
||||
}
|
||||
|
||||
@@ -53,16 +53,14 @@ fn consume_pending_then_paint(delegate: &HostWebViewDelegate, paint: impl FnOnce
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
use ely_domain::ProfileId;
|
||||
|
||||
use super::{HostWebViewDelegate, consume_pending_then_paint};
|
||||
use crate::runtime_permissions::PermissionStore;
|
||||
|
||||
#[test]
|
||||
fn frame_arriving_during_paint_remains_pending() {
|
||||
let delegate =
|
||||
HostWebViewDelegate::new(ProfileId::new(), Rc::new(RefCell::new(HashMap::new())));
|
||||
let delegate = HostWebViewDelegate::new(ProfileId::new(), PermissionStore::default());
|
||||
delegate.mark_frame_ready();
|
||||
|
||||
consume_pending_then_paint(&delegate, || delegate.mark_frame_ready());
|
||||
|
||||
@@ -3,9 +3,25 @@ use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature};
|
||||
use servo::WebView;
|
||||
|
||||
use crate::{PermissionDecision, PermissionRequest};
|
||||
use crate::{
|
||||
ConsumedPermission, PermissionDecision, PermissionSnapshotEntry, PermissionSnapshotState,
|
||||
};
|
||||
|
||||
pub(super) type PermissionStore = Rc<RefCell<HashMap<PermissionKey, PermissionDecision>>>;
|
||||
pub(super) type PermissionStore = Rc<RefCell<PermissionState>>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct PermissionState {
|
||||
entries: HashMap<PermissionKey, StoredPermission>,
|
||||
generations: HashMap<ProfileId, u64>,
|
||||
consumed: Vec<ConsumedPermission>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct StoredPermission {
|
||||
decision: PermissionDecision,
|
||||
grant_revision: u64,
|
||||
consumed: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub(super) struct PermissionKey {
|
||||
@@ -20,14 +36,64 @@ impl PermissionKey {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_permission_decision(
|
||||
pub(super) fn replace_permission_decisions(
|
||||
permissions: &PermissionStore,
|
||||
request: PermissionRequest,
|
||||
decision: PermissionDecision,
|
||||
profile_id: &ProfileId,
|
||||
generation: u64,
|
||||
entries: Vec<PermissionSnapshotEntry>,
|
||||
) {
|
||||
permissions
|
||||
.borrow_mut()
|
||||
.insert(PermissionKey::new(request.profile_id, request.origin, request.feature), decision);
|
||||
let mut state = permissions.borrow_mut();
|
||||
if state.generations.get(profile_id).is_some_and(|current| generation < *current) {
|
||||
return;
|
||||
}
|
||||
let mut freshly_consumed = Vec::new();
|
||||
let replacements = entries
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
let key = PermissionKey::new(profile_id.clone(), entry.origin, entry.feature);
|
||||
let stored = match entry.state {
|
||||
PermissionSnapshotState::Decision(decision) => {
|
||||
let consumed = decision == PermissionDecision::AllowOnce
|
||||
&& state.entries.get(&key).is_some_and(|current| {
|
||||
current.decision == PermissionDecision::AllowOnce
|
||||
&& current.grant_revision == entry.revision
|
||||
&& current.consumed
|
||||
});
|
||||
StoredPermission { decision, grant_revision: entry.revision, consumed }
|
||||
}
|
||||
PermissionSnapshotState::TransferredAllowOnce => match state.entries.get(&key) {
|
||||
Some(current)
|
||||
if current.decision == PermissionDecision::AllowOnce
|
||||
&& current.grant_revision == entry.revision =>
|
||||
{
|
||||
StoredPermission {
|
||||
decision: PermissionDecision::AllowOnce,
|
||||
grant_revision: current.grant_revision,
|
||||
consumed: current.consumed,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
freshly_consumed.push(ConsumedPermission {
|
||||
profile_id: profile_id.clone(),
|
||||
origin: key.origin.clone(),
|
||||
feature: key.feature,
|
||||
grant_revision: entry.revision,
|
||||
});
|
||||
StoredPermission {
|
||||
decision: PermissionDecision::AllowOnce,
|
||||
grant_revision: entry.revision,
|
||||
consumed: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
(key, stored)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
state.entries.retain(|key, _| &key.profile_id != profile_id);
|
||||
state.entries.extend(replacements);
|
||||
state.consumed.extend(freshly_consumed);
|
||||
state.generations.insert(profile_id.clone(), generation);
|
||||
}
|
||||
|
||||
pub(super) fn permission_decision_for_webview(
|
||||
@@ -49,11 +115,31 @@ fn take_permission_decision(
|
||||
feature: SitePermissionFeature,
|
||||
) -> Option<PermissionDecision> {
|
||||
let key = PermissionKey::new(profile_id.clone(), origin, feature);
|
||||
let mut permissions = permissions.borrow_mut();
|
||||
match permissions.get(&key).cloned() {
|
||||
Some(PermissionDecision::AllowOnce) => permissions.remove(&key),
|
||||
decision => decision,
|
||||
let mut state = permissions.borrow_mut();
|
||||
let (decision, grant_revision) = {
|
||||
let stored = state.entries.get_mut(&key)?;
|
||||
match stored.decision {
|
||||
PermissionDecision::AllowOnce if stored.consumed => return None,
|
||||
PermissionDecision::AllowOnce => {
|
||||
stored.consumed = true;
|
||||
(PermissionDecision::AllowOnce, Some(stored.grant_revision))
|
||||
}
|
||||
decision => (decision, None),
|
||||
}
|
||||
};
|
||||
if let Some(grant_revision) = grant_revision {
|
||||
state.consumed.push(ConsumedPermission {
|
||||
profile_id: key.profile_id,
|
||||
origin: key.origin,
|
||||
feature: key.feature,
|
||||
grant_revision,
|
||||
});
|
||||
}
|
||||
Some(decision)
|
||||
}
|
||||
|
||||
pub(super) fn drain_consumed_permissions(permissions: &PermissionStore) -> Vec<ConsumedPermission> {
|
||||
std::mem::take(&mut permissions.borrow_mut().consumed)
|
||||
}
|
||||
|
||||
fn site_origin_for_webview(webview: &WebView, fallback_url: Option<String>) -> Option<SiteOrigin> {
|
||||
@@ -87,121 +173,5 @@ fn site_permission_feature_for_servo(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature};
|
||||
|
||||
use crate::{PermissionDecision, PermissionRequest};
|
||||
|
||||
use super::{
|
||||
PermissionStore, set_permission_decision, site_permission_feature_for_servo,
|
||||
take_permission_decision,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn keeps_disabled_servo_permissions_out_of_site_settings() {
|
||||
for feature in [
|
||||
servo::PermissionFeature::ScreenWakeLock(servo::WakeLockType::Screen),
|
||||
servo::PermissionFeature::Gamepad,
|
||||
] {
|
||||
assert_eq!(site_permission_feature_for_servo(feature), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_once_is_consumed_after_one_matching_origin_request()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let permissions = PermissionStore::default();
|
||||
let profile_id = ProfileId::new();
|
||||
let origin = SiteOrigin::parse("https://example.com/path")?;
|
||||
|
||||
set_permission_decision(
|
||||
&permissions,
|
||||
PermissionRequest {
|
||||
webview_id: ely_domain::WebViewId::new(),
|
||||
profile_id: profile_id.clone(),
|
||||
origin: origin.clone(),
|
||||
feature: SitePermissionFeature::Camera,
|
||||
},
|
||||
PermissionDecision::AllowOnce,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
take_permission_decision(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
),
|
||||
Some(PermissionDecision::AllowOnce)
|
||||
);
|
||||
assert_eq!(
|
||||
take_permission_decision(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
origin,
|
||||
SitePermissionFeature::Camera
|
||||
),
|
||||
None
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_always_stays_scoped_to_profile_origin_and_feature()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let permissions = PermissionStore::default();
|
||||
let profile_id = ProfileId::new();
|
||||
let other_profile_id = ProfileId::new();
|
||||
let origin = SiteOrigin::parse("https://example.com")?;
|
||||
let other_origin = SiteOrigin::parse("https://example.org")?;
|
||||
|
||||
set_permission_decision(
|
||||
&permissions,
|
||||
PermissionRequest {
|
||||
webview_id: ely_domain::WebViewId::new(),
|
||||
profile_id: profile_id.clone(),
|
||||
origin: origin.clone(),
|
||||
feature: SitePermissionFeature::Notifications,
|
||||
},
|
||||
PermissionDecision::AllowAlways,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
take_permission_decision(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Notifications,
|
||||
),
|
||||
Some(PermissionDecision::AllowAlways)
|
||||
);
|
||||
assert_eq!(
|
||||
take_permission_decision(
|
||||
&permissions,
|
||||
&other_profile_id,
|
||||
origin,
|
||||
SitePermissionFeature::Notifications,
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
take_permission_decision(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
other_origin,
|
||||
SitePermissionFeature::Notifications,
|
||||
),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
take_permission_decision(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
SiteOrigin::parse("https://example.com")?,
|
||||
SitePermissionFeature::Camera,
|
||||
),
|
||||
None
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[path = "runtime_permissions_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
use ely_domain::{ProfileId, SiteOrigin, SitePermissionFeature};
|
||||
|
||||
use crate::{
|
||||
ConsumedPermission, PermissionDecision, PermissionSnapshotEntry, PermissionSnapshotState,
|
||||
runtime_permissions::{
|
||||
PermissionStore, drain_consumed_permissions, replace_permission_decisions,
|
||||
site_permission_feature_for_servo, take_permission_decision,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn keeps_disabled_servo_permissions_out_of_site_settings() {
|
||||
for feature in [
|
||||
servo::PermissionFeature::ScreenWakeLock(servo::WakeLockType::Screen),
|
||||
servo::PermissionFeature::Gamepad,
|
||||
] {
|
||||
assert_eq!(site_permission_feature_for_servo(feature), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_once_is_consumed_after_one_matching_request() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let permissions = PermissionStore::default();
|
||||
let profile_id = ProfileId::new();
|
||||
let origin = SiteOrigin::parse("https://example.com")?;
|
||||
replace(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
1,
|
||||
vec![decision(
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionDecision::AllowOnce,
|
||||
1,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_eq!(take(&permissions, &profile_id, &origin), Some(PermissionDecision::AllowOnce));
|
||||
assert_eq!(take(&permissions, &profile_id, &origin), None);
|
||||
assert_eq!(
|
||||
drain_consumed_permissions(&permissions),
|
||||
vec![ConsumedPermission {
|
||||
profile_id,
|
||||
origin,
|
||||
feature: SitePermissionFeature::Camera,
|
||||
grant_revision: 1,
|
||||
}],
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn durable_permissions_stay_scoped_to_profile_origin_and_feature()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let permissions = PermissionStore::default();
|
||||
let profile_id = ProfileId::new();
|
||||
let other_profile = ProfileId::new();
|
||||
let origin = SiteOrigin::parse("https://example.com")?;
|
||||
replace(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
1,
|
||||
vec![decision(
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionDecision::AllowAlways,
|
||||
1,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_eq!(take(&permissions, &profile_id, &origin), Some(PermissionDecision::AllowAlways));
|
||||
assert_eq!(take(&permissions, &other_profile, &origin), None);
|
||||
assert_eq!(
|
||||
take_permission_decision(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
SiteOrigin::parse("https://other.test")?,
|
||||
SitePermissionFeature::Camera,
|
||||
),
|
||||
None,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authoritative_snapshot_removes_missing_durable_entries() -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let permissions = PermissionStore::default();
|
||||
let profile_id = ProfileId::new();
|
||||
let other_profile = ProfileId::new();
|
||||
let origin = SiteOrigin::parse("https://example.com")?;
|
||||
let other_origin = SiteOrigin::parse("https://other.test")?;
|
||||
replace(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
1,
|
||||
vec![decision(
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionDecision::AllowAlways,
|
||||
1,
|
||||
)],
|
||||
);
|
||||
replace(
|
||||
&permissions,
|
||||
&other_profile,
|
||||
1,
|
||||
vec![decision(
|
||||
other_origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionDecision::DenyAlways,
|
||||
1,
|
||||
)],
|
||||
);
|
||||
|
||||
replace(&permissions, &profile_id, 2, Vec::new());
|
||||
|
||||
assert_eq!(take(&permissions, &profile_id, &origin), None);
|
||||
assert_eq!(
|
||||
take(&permissions, &other_profile, &other_origin),
|
||||
Some(PermissionDecision::DenyAlways)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transferred_marker_preserves_existing_token_and_fails_closed_for_fresh_host()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let profile_id = ProfileId::new();
|
||||
let origin = SiteOrigin::parse("https://example.com")?;
|
||||
let marker = state(
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionSnapshotState::TransferredAllowOnce,
|
||||
1,
|
||||
);
|
||||
let live = PermissionStore::default();
|
||||
replace(
|
||||
&live,
|
||||
&profile_id,
|
||||
1,
|
||||
vec![decision(
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionDecision::AllowOnce,
|
||||
1,
|
||||
)],
|
||||
);
|
||||
replace(&live, &profile_id, 2, vec![marker.clone()]);
|
||||
|
||||
let restarted = PermissionStore::default();
|
||||
replace(&restarted, &profile_id, 2, vec![marker]);
|
||||
|
||||
assert!(drain_consumed_permissions(&live).is_empty());
|
||||
assert_eq!(
|
||||
drain_consumed_permissions(&restarted),
|
||||
vec![ConsumedPermission {
|
||||
profile_id: profile_id.clone(),
|
||||
origin: origin.clone(),
|
||||
feature: SitePermissionFeature::Camera,
|
||||
grant_revision: 1,
|
||||
}],
|
||||
);
|
||||
assert_eq!(take(&live, &profile_id, &origin), Some(PermissionDecision::AllowOnce));
|
||||
assert_eq!(take(&restarted, &profile_id, &origin), None);
|
||||
assert_eq!(
|
||||
drain_consumed_permissions(&live),
|
||||
vec![ConsumedPermission {
|
||||
profile_id,
|
||||
origin,
|
||||
feature: SitePermissionFeature::Camera,
|
||||
grant_revision: 1,
|
||||
}],
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_snapshot_removes_transferred_allow_once() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let permissions = PermissionStore::default();
|
||||
let profile_id = ProfileId::new();
|
||||
let origin = SiteOrigin::parse("https://example.com")?;
|
||||
replace(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
1,
|
||||
vec![decision(
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionDecision::AllowOnce,
|
||||
1,
|
||||
)],
|
||||
);
|
||||
replace(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
2,
|
||||
vec![state(
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionSnapshotState::TransferredAllowOnce,
|
||||
1,
|
||||
)],
|
||||
);
|
||||
replace(&permissions, &profile_id, 3, Vec::new());
|
||||
|
||||
assert_eq!(take(&permissions, &profile_id, &origin), None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mismatched_transferred_revision_consumes_the_replacement_token()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let permissions = PermissionStore::default();
|
||||
let profile_id = ProfileId::new();
|
||||
let origin = SiteOrigin::parse("https://example.com")?;
|
||||
replace(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
1,
|
||||
vec![decision(
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionDecision::AllowOnce,
|
||||
1,
|
||||
)],
|
||||
);
|
||||
replace(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
2,
|
||||
vec![state(
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionSnapshotState::TransferredAllowOnce,
|
||||
2,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_eq!(take(&permissions, &profile_id, &origin), None);
|
||||
assert_eq!(
|
||||
drain_consumed_permissions(&permissions),
|
||||
vec![ConsumedPermission {
|
||||
profile_id,
|
||||
origin,
|
||||
feature: SitePermissionFeature::Camera,
|
||||
grant_revision: 2,
|
||||
}],
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_profile_generation_cannot_restore_revoked_permission()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let permissions = PermissionStore::default();
|
||||
let profile_id = ProfileId::new();
|
||||
let origin = SiteOrigin::parse("https://example.com")?;
|
||||
let old =
|
||||
decision(origin.clone(), SitePermissionFeature::Camera, PermissionDecision::AllowAlways, 1);
|
||||
replace(&permissions, &profile_id, 1, vec![old.clone()]);
|
||||
replace(&permissions, &profile_id, 2, Vec::new());
|
||||
|
||||
replace(&permissions, &profile_id, 1, vec![old]);
|
||||
|
||||
assert_eq!(take(&permissions, &profile_id, &origin), None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_revision_stays_consumed_and_new_revision_rearms_token()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let permissions = PermissionStore::default();
|
||||
let profile_id = ProfileId::new();
|
||||
let origin = SiteOrigin::parse("https://example.com")?;
|
||||
replace(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
1,
|
||||
vec![decision(
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionDecision::AllowOnce,
|
||||
1,
|
||||
)],
|
||||
);
|
||||
assert_eq!(take(&permissions, &profile_id, &origin), Some(PermissionDecision::AllowOnce));
|
||||
replace(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
2,
|
||||
vec![decision(
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionDecision::AllowOnce,
|
||||
1,
|
||||
)],
|
||||
);
|
||||
assert_eq!(take(&permissions, &profile_id, &origin), None);
|
||||
replace(
|
||||
&permissions,
|
||||
&profile_id,
|
||||
3,
|
||||
vec![decision(
|
||||
origin.clone(),
|
||||
SitePermissionFeature::Camera,
|
||||
PermissionDecision::AllowOnce,
|
||||
3,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_eq!(take(&permissions, &profile_id, &origin), Some(PermissionDecision::AllowOnce));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn replace(
|
||||
permissions: &PermissionStore,
|
||||
profile_id: &ProfileId,
|
||||
generation: u64,
|
||||
entries: Vec<PermissionSnapshotEntry>,
|
||||
) {
|
||||
replace_permission_decisions(permissions, profile_id, generation, entries);
|
||||
}
|
||||
|
||||
fn decision(
|
||||
origin: SiteOrigin,
|
||||
feature: SitePermissionFeature,
|
||||
decision: PermissionDecision,
|
||||
revision: u64,
|
||||
) -> PermissionSnapshotEntry {
|
||||
state(origin, feature, PermissionSnapshotState::Decision(decision), revision)
|
||||
}
|
||||
|
||||
fn state(
|
||||
origin: SiteOrigin,
|
||||
feature: SitePermissionFeature,
|
||||
state: PermissionSnapshotState,
|
||||
revision: u64,
|
||||
) -> PermissionSnapshotEntry {
|
||||
PermissionSnapshotEntry { origin, feature, state, revision }
|
||||
}
|
||||
|
||||
fn take(
|
||||
permissions: &PermissionStore,
|
||||
profile_id: &ProfileId,
|
||||
origin: &SiteOrigin,
|
||||
) -> Option<PermissionDecision> {
|
||||
take_permission_decision(permissions, profile_id, origin.clone(), SitePermissionFeature::Camera)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use servo::Preferences;
|
||||
|
||||
pub(super) fn ely_servo_preferences() -> Preferences {
|
||||
// Servo's current permission request omits the requesting principal. WebRTC,
|
||||
// Clipboard, and Bluetooth also contain paths that bypass the embedder broker.
|
||||
// Keep every affected API explicitly gated until those contracts are complete.
|
||||
Preferences {
|
||||
dom_async_clipboard_enabled: false,
|
||||
dom_bluetooth_enabled: false,
|
||||
dom_geolocation_enabled: false,
|
||||
dom_notification_enabled: false,
|
||||
dom_permissions_enabled: false,
|
||||
dom_storage_manager_api_enabled: false,
|
||||
dom_wakelock_enabled: false,
|
||||
dom_webrtc_enabled: false,
|
||||
dom_intersection_observer_enabled: true,
|
||||
layout_grid_enabled: true,
|
||||
layout_variable_fonts_enabled: true,
|
||||
..Preferences::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ely_servo_preferences;
|
||||
|
||||
#[test]
|
||||
fn unsafe_permission_surfaces_stay_gated() {
|
||||
let preferences = ely_servo_preferences();
|
||||
|
||||
assert!(!preferences.dom_async_clipboard_enabled);
|
||||
assert!(!preferences.dom_bluetooth_enabled);
|
||||
assert!(!preferences.dom_geolocation_enabled);
|
||||
assert!(!preferences.dom_notification_enabled);
|
||||
assert!(!preferences.dom_permissions_enabled);
|
||||
assert!(!preferences.dom_storage_manager_api_enabled);
|
||||
assert!(!preferences.dom_wakelock_enabled);
|
||||
assert!(!preferences.dom_webrtc_enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_rendering_features_stay_enabled() {
|
||||
let preferences = ely_servo_preferences();
|
||||
|
||||
assert!(preferences.dom_intersection_observer_enabled);
|
||||
assert!(preferences.layout_grid_enabled);
|
||||
assert!(preferences.layout_variable_fonts_enabled);
|
||||
}
|
||||
}
|
||||
@@ -184,16 +184,13 @@ impl WebViewDelegate for HostWebViewDelegate {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
|
||||
use ely_domain::ProfileId;
|
||||
|
||||
use super::HostWebViewDelegate;
|
||||
use super::{HostWebViewDelegate, PermissionStore};
|
||||
|
||||
#[test]
|
||||
fn metadata_changes_are_separate_from_pending_frame() {
|
||||
let delegate =
|
||||
HostWebViewDelegate::new(ProfileId::new(), Rc::new(RefCell::new(HashMap::new())));
|
||||
let delegate = HostWebViewDelegate::new(ProfileId::new(), PermissionStore::default());
|
||||
|
||||
assert!(!delegate.has_pending_frame());
|
||||
assert!(!delegate.has_pending_metadata());
|
||||
|
||||
Reference in New Issue
Block a user