--- original +++ modified @@ -7,8 +7,8 @@ use base::id::{BrowsingContextId, PipelineId, WebViewId}; use constellation_traits::{ - IFrameLoadInfo, IFrameLoadInfoWithData, JsEvalResult, LoadData, LoadOrigin, - NavigationHistoryBehavior, ScriptToConstellationMessage, + EmbeddedWebViewCreationRequest, IFrameLoadInfo, IFrameLoadInfoWithData, JsEvalResult, LoadData, + LoadOrigin, NavigationHistoryBehavior, ScriptToConstellationMessage, }; use content_security_policy::sandboxing_directive::{ SandboxingFlagSet, parse_a_sandboxing_directive, @@ -16,6 +16,7 @@ use dom_struct::dom_struct; use embedder_traits::ViewportDetails; use html5ever::{LocalName, Prefix, local_name, ns}; +use ipc_channel::ipc; use js::rust::HandleObject; use net_traits::ReferrerPolicy; use net_traits::request::Destination; @@ -22,30 +23,35 @@ use profile_traits::ipc as ProfiledIpc; use script_traits::{NewPipelineInfo, UpdatePipelineIdReason}; use servo_url::ServoUrl; +use style::Atom; use style::attr::{AttrValue, LengthOrPercentageOrAuto}; -use stylo_atoms::Atom; use crate::document_loader::{LoadBlocker, LoadType}; use crate::dom::attr::Attr; use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::EmbeddedWebViewBinding::ScreenshotOptions; use crate::dom::bindings::codegen::Bindings::HTMLIFrameElementBinding::HTMLIFrameElementMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods; use crate::dom::bindings::codegen::UnionTypes::TrustedHTMLOrString; use crate::dom::bindings::error::Fallible; use crate::dom::bindings::inheritance::Castable; +use crate::dom::bindings::num::Finite; use crate::dom::bindings::reflector::DomGlobal; use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom}; use crate::dom::bindings::str::{DOMString, USVString}; +use crate::dom::console::Console; use crate::dom::document::Document; use crate::dom::domtokenlist::DOMTokenList; use crate::dom::element::{ AttributeMutation, Element, LayoutElementHelpers, reflect_referrer_policy_attribute, }; +use crate::dom::embedder::Embedder; use crate::dom::eventtarget::EventTarget; use crate::dom::globalscope::GlobalScope; use crate::dom::html::htmlelement::HTMLElement; use crate::dom::node::{BindContext, Node, NodeDamage, NodeTraits, UnbindContext}; use crate::dom::performance::performanceresourcetiming::InitiatorType; +use crate::dom::promise::Promise; use crate::dom::trustedhtml::TrustedHTML; use crate::dom::virtualmethods::VirtualMethods; use crate::dom::windowproxy::WindowProxy; @@ -66,6 +72,12 @@ NotFirstTime, } +// Re-export PendingDialogSender from the embedded webview module +pub(crate) use super::htmlembeddedwebview::PendingDialogSender; +// Type alias for pending permission senders +pub(crate) type PendingPermissionSender = + base::generic_channel::GenericSender; + #[dom_struct] pub(crate) struct HTMLIFrameElement { htmlelement: HTMLElement, @@ -97,6 +109,30 @@ /// an empty iframe is attached. In that case, we shouldn't fire a /// subsequent asynchronous load event. already_fired_synchronous_load_event: Cell, + /// Whether this iframe is in "embed" mode (hosting a top-level webview). + is_embedded_webview: Cell, + /// The embedded webview ID when in embed mode. + #[no_trace] + embedded_webview_id: Cell>, + /// History tracking for embedded webviews - the list of URLs in the session history. + #[no_trace] + embedded_history: DomRefCell>, + /// The current index in the embedded webview history. + embedded_history_index: Cell, + /// Pending dialog sender for embedded webviews. + /// When an embedded webview calls alert/confirm/prompt, the IPC sender is stored here + /// so the parent shell can respond directly via respondToAlert/Confirm/Prompt. + #[ignore_malloc_size_of = "Channels are hard"] + #[no_trace] + pending_dialog: DomRefCell>, + /// Pending permission sender for embedded webviews. + /// When an embedded webview requests a permission, the IPC sender is stored here + /// so the parent shell can respond directly via respondToPermissionPrompt. + #[ignore_malloc_size_of = "Channels are hard"] + #[no_trace] + pending_permission_sender: DomRefCell>, + /// Current page zoom level for embedded webviews (default 1.0 = 100%). + page_zoom: Cell, } impl HTMLIFrameElement { @@ -255,6 +291,8 @@ viewport_details, user_content_manager_id: None, theme: window.theme(), + is_embedded_webview: false, + hide_focus: false, }; self.pipeline_id.set(Some(new_pipeline_id)); @@ -484,6 +522,147 @@ ); } + /// Create an embedded webview for this iframe when the "embed" attribute is present. + /// This creates a new top-level WebView instead of a nested browsing context. + fn create_embedded_webview(&self) { + let Some(url) = self.shared_attribute_processing_steps_for_iframe_and_frame_elements() + else { + error!("Can't create embedded webview without url"); + return; + }; + let document = self.owner_document(); + let window = self.owner_window(); + + // Check if the parent document's origin is allowed to create embedded webviews + let parent_url = document.url(); + if !Embedder::is_allowed_to_embed_for_url(&parent_url) { + let message = format!( + "Embedded webview creation blocked: the 'embed' attribute on iframes is only \ + allowed for privileged origins. Current origin '{}' is not allowed.", + parent_url.origin().ascii_serialization() + ); + // Log error to web console so developers can see why it failed + Console::internal_warn(window.as_global_scope(), DOMString::from(message.clone())); + warn!("{}", message); + + // Fall back to non-functional state (iframe won't navigate) + self.is_embedded_webview.set(false); + return; + } + + let pipeline_id = window.pipeline_id(); + let parent_webview_id = window.window_proxy().webview_id(); + + // Create load data for the embedded webview + let mut load_data = LoadData::new( + LoadOrigin::Script(document.origin().snapshot()), + url, + Some(document.base_url()), + Some(pipeline_id), + window.as_global_scope().get_referrer(), + document.get_referrer_policy(), + Some(window.as_global_scope().is_secure_context()), + Some(document.insecure_requests_policy()), + document.has_trustworthy_ancestor_or_current_origin(), + self.sandboxing_flag_set(), + ); + load_data.destination = Destination::IFrame; + load_data.policy_container = Some(window.as_global_scope().policy_container()); + + // Clone load_data for spawning the pipeline later + let load_data_for_spawn = load_data.clone(); + let theme = window.theme(); + + // Get the iframe's size to use as the viewport for the embedded webview. + // We use border_box which gives us the size in CSS pixels. + let hidpi_scale_factor = window.device_pixel_ratio(); + let viewport_details = self + .upcast::() + .border_box() + .map(|border_box| ViewportDetails { + size: euclid::Size2D::new( + border_box.size.width.to_f32_px(), + border_box.size.height.to_f32_px(), + ), + hidpi_scale_factor, + page_zoom_for_rendering: None, + }) + .unwrap_or_else(|| ViewportDetails { + hidpi_scale_factor, + ..Default::default() + }); + + // Create an IPC channel for the response + let (response_sender, response_receiver) = + ipc::channel().expect("Failed to create IPC channel for embedded webview"); + + let hide_focus = self.has_hide_focus(); + let request = EmbeddedWebViewCreationRequest { + load_data, + parent_pipeline_id: pipeline_id, + parent_webview_id, + viewport_details, + theme, + hide_focus, + response_sender, + }; + + // Send the request to the constellation + let global = window.as_global_scope(); + let msg = ScriptToConstellationMessage::CreateEmbeddedWebView(request); + global.script_to_constellation_chan().send(msg).unwrap(); + + // Block waiting for the response + match response_receiver.recv() { + Ok(Some(response)) => { + debug!( + "Embedded webview created: webview_id={:?}, browsing_context_id={:?}, pipeline_id={:?}", + response.new_webview_id, + response.new_browsing_context_id, + response.new_pipeline_id + ); + // Store the embedded webview state + self.is_embedded_webview.set(true); + self.embedded_webview_id.set(Some(response.new_webview_id)); + self.browsing_context_id + .set(Some(response.new_browsing_context_id)); + self.pipeline_id.set(Some(response.new_pipeline_id)); + self.webview_id.set(Some(response.new_webview_id)); + + // Spawn the pipeline in the script thread + // Embedded webviews are top-level, so parent_info is None + let new_pipeline_info = NewPipelineInfo { + parent_info: None, + new_pipeline_id: response.new_pipeline_id, + browsing_context_id: response.new_browsing_context_id, + webview_id: response.new_webview_id, + opener: None, + load_data: load_data_for_spawn, + viewport_details, + user_content_manager_id: None, + theme, + is_embedded_webview: true, + hide_focus, + }; + + with_script_thread(|script_thread| { + script_thread.spawn_pipeline(new_pipeline_info); + }); + }, + Ok(None) => { + warn!("Embedded webview creation was rejected by embedder"); + // Fall back to not being an embedded webview (just a static iframe) + self.is_embedded_webview.set(false); + self.embedded_webview_id.set(None); + }, + Err(e) => { + warn!("Failed to receive embedded webview response: {:?}", e); + self.is_embedded_webview.set(false); + self.embedded_webview_id.set(None); + }, + } + } + fn destroy_nested_browsing_context(&self) { self.pipeline_id.set(None); self.pending_pipeline_id.set(None); @@ -544,6 +723,13 @@ script_window_proxies: ScriptThread::window_proxies(), pending_navigation: Default::default(), already_fired_synchronous_load_event: Default::default(), + is_embedded_webview: Cell::new(false), + embedded_webview_id: Cell::new(None), + embedded_history: DomRefCell::new(Vec::new()), + embedded_history_index: Cell::new(0), + pending_dialog: DomRefCell::new(None), + pending_permission_sender: DomRefCell::new(None), + page_zoom: Cell::new(1.0), } } @@ -579,6 +765,149 @@ self.webview_id.get() } + /// Check whether this iframe has the "embed" attribute set. + fn is_embed_mode(&self) -> bool { + self.upcast::() + .has_attribute(&local_name!("embed")) + } + + /// Check whether this iframe has the "hidefocus" attribute set. + /// When set, the embedded webview should never receive focus. + fn has_hide_focus(&self) -> bool { + self.upcast::() + .has_attribute(&local_name!("hidefocus")) + } + + /// Check if this iframe should adopt a pre-created embedded webview. + /// This is set via the `adopt-webview-id`, `adopt-browsing-context-id`, + /// and `adopt-pipeline-id` attributes, which are used when the constellation + /// has already created an embedded webview (e.g., from window.open in an embedded context). + fn get_adopt_ids(&self) -> Option<(WebViewId, BrowsingContextId, PipelineId)> { + let element = self.upcast::(); + + // All three attributes must be present + let webview_id_str = element.get_string_attribute(&LocalName::from("adopt-webview-id")); + let browsing_context_id_str = + element.get_string_attribute(&LocalName::from("adopt-browsing-context-id")); + let pipeline_id_str = element.get_string_attribute(&LocalName::from("adopt-pipeline-id")); + + if webview_id_str.is_empty() || + browsing_context_id_str.is_empty() || + pipeline_id_str.is_empty() + { + return None; + } + + // Parse the IDs (they are in the format "TypeName(namespace,index)") + let browsing_context_id = BrowsingContextId::from_string(&browsing_context_id_str.str())?; + let pipeline_id = PipelineId::from_string(&pipeline_id_str.str())?; + + // WebViewId is "(painter_id, browsing_context_id)" - we derive it from the browsing_context_id + // The WebViewId is constructed from the same BrowsingContextId + let webview_id = WebViewId::from_string(&webview_id_str.str())?; + + Some((webview_id, browsing_context_id, pipeline_id)) + } + + /// Adopt a pre-created embedded webview. This is called when the iframe has + /// adopt attributes set, indicating that constellation has already created + /// the webview and we just need to associate it with this iframe. + fn adopt_embedded_webview( + &self, + webview_id: WebViewId, + browsing_context_id: BrowsingContextId, + pipeline_id: PipelineId, + can_gc: CanGc, + ) { + debug!( + "