/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/script/dom/window/windowproxy.rs
1 503 строки
55 KB
Josh Matthews
script: Do not navigate a newly-opened window to about:blank. (#47074)
08 авг 2026, 13:06
Не верифицирован
08 авг 2026, 13:06
d4ed3ed
Код
Авторство
О чём код?
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::Cell; use std::ptr::{self, NonNull}; use std::rc::Rc; use content_security_policy::sandboxing_directive::SandboxingFlagSet; use dom_struct::dom_struct; use html5ever::local_name; use indexmap::map::IndexMap; use js::JSCLASS_IS_GLOBAL; use js::context::JSContext; use js::glue::{ CreateWrapperProxyHandler, DeleteWrapperProxyHandler, GetProxyPrivate, GetProxyReservedSlot, ProxyTraps, SetProxyReservedSlot, }; use js::jsapi::{ GCContext, Handle as RawHandle, HandleId as RawHandleId, HandleObject as RawHandleObject, HandleValue as RawHandleValue, JS_DefinePropertyById, JS_ForwardSetPropertyTo, JSContext as RawJSContext, JSErrNum, JSObject, JSPROP_ENUMERATE, JSPROP_READONLY, JSTracer, MutableHandle as RawMutableHandle, MutableHandleObject as RawMutableHandleObject, MutableHandleValue as RawMutableHandleValue, ObjectOpResult, PropertyDescriptor, }; use js::jsval::{NullValue, PrivateValue, UndefinedValue}; use js::realm::{AutoRealm, CurrentRealm}; use js::rust::wrappers2::{ JS_ForwardGetPropertyTo, JS_GetOwnPropertyDescriptorById, JS_HasOwnPropertyById, JS_HasPropertyById, JS_IsExceptionPending, JS_TransplantObject, NewWindowProxy, SetWindowProxy, }; use js::rust::{Handle, MutableHandle, MutableHandleValue, get_object_class}; use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; use net_traits::ReferrerPolicy; use net_traits::request::Referrer; use script_bindings::cell::DomRefCell; use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods; use script_bindings::proxyhandler::set_property_descriptor; use script_bindings::reflector::{DomObject, MutDomObject, Reflector}; use script_traits::NewPipelineInfo; use serde::{Deserialize, Serialize}; use servo_base::generic_channel; use servo_base::generic_channel::GenericSend; use servo_base::id::{BrowsingContextId, PipelineId, WebViewId}; use servo_constellation_traits::{ AuxiliaryWebViewCreationRequest, LoadData, LoadOrigin, NavigationHistoryBehavior, ScriptToConstellationMessage, TargetSnapshotParams, }; use servo_url::{ImmutableOrigin, ServoUrl}; use storage_traits::webstorage_thread::WebStorageThreadMsg; use style::attr::parse_integer; use crate::dom::bindings::conversions::{ToJSValConvertible, root_from_handleobject}; use crate::dom::bindings::error::{Error, Fallible, throw_dom_exception}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::reflector::DomGlobal; use crate::dom::bindings::root::{Dom, DomRoot}; use crate::dom::bindings::settings_stack::maybe_entry_global; use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::bindings::trace::JSTraceable; use crate::dom::bindings::utils::get_array_index_from_id; use crate::dom::dissimilaroriginwindow::DissimilarOriginWindow; use crate::dom::document::Document; use crate::dom::element::Element; use crate::dom::globalscope::GlobalScope; use crate::dom::window::Window; use crate::navigation::navigate; use crate::script_thread::{ScriptThread, with_script_thread}; use crate::script_window_proxies::ScriptWindowProxies; #[dom_struct] // NOTE: the browsing context for a window is managed in two places: // here, in script, but also in the constellation. The constellation // manages the session history, which in script is accessed through // History objects, messaging the constellation. pub(crate) struct WindowProxy { /// The JS WindowProxy object. /// Unlike other reflectors, we mutate this field because /// we have to brain-transplant the reflector when the WindowProxy /// changes Window. reflector: Reflector, /// The id of the browsing context. /// In the case that this is a nested browsing context, this is the id /// of the container. #[no_trace] browsing_context_id: BrowsingContextId, // https://html.spec.whatwg.org/multipage/#opener-browsing-context #[no_trace] opener: Option<BrowsingContextId>, /// The frame id of the top-level ancestor browsing context. /// In the case that this is a top-level window, this is our id. #[no_trace] webview_id: WebViewId, /// The name of the browsing context (sometimes, but not always, /// equal to the name of a container element) name: DomRefCell<DOMString>, /// The pipeline id of the currently active document. /// May be None, when the currently active document is in another script thread. /// We do not try to keep the pipeline id for documents in other threads, /// as this would require the constellation notifying many script threads about /// the change, which could be expensive. #[no_trace] currently_active: Cell<Option<PipelineId>>, /// Has the browsing context been discarded? discarded: Cell<bool>, /// Has the browsing context been disowned? disowned: Cell<bool>, /// <https://html.spec.whatwg.org/multipage/#is-closing> is_closing: Cell<bool>, /// If the containing `<iframe>` of this [`WindowProxy`] is from a same-origin page, /// this will be the [`Element`] of the `<iframe>` element in the realm of the /// parent page. Otherwise, it is `None`. frame_element: Option<Dom<Element>>, /// The parent browsing context's window proxy, if this is a nested browsing context parent: Option<Dom<WindowProxy>>, /// <https://html.spec.whatwg.org/multipage/#delaying-load-events-mode> delaying_load_events_mode: Cell<bool>, /// The creator browsing context's url. #[no_trace] creator_url: Option<ServoUrl>, /// The creator browsing context's origin. #[no_trace] creator_origin: Option<ImmutableOrigin>, /// The window proxies the script thread knows. #[conditional_malloc_size_of] script_window_proxies: Rc<ScriptWindowProxies>, } impl WindowProxy { fn new_inherited( browsing_context_id: BrowsingContextId, webview_id: WebViewId, currently_active: Option<PipelineId>, frame_element: Option<&Element>, parent: Option<&WindowProxy>, opener: Option<BrowsingContextId>, creator: CreatorBrowsingContextInfo, ) -> WindowProxy { let name = frame_element.map_or(DOMString::new(), |e| { e.get_string_attribute(&local_name!("name")) }); WindowProxy { reflector: Reflector::new(), browsing_context_id, webview_id, name: DomRefCell::new(name), currently_active: Cell::new(currently_active), discarded: Cell::new(false), disowned: Cell::new(false), is_closing: Cell::new(false), frame_element: frame_element.map(Dom::from_ref), parent: parent.map(Dom::from_ref), delaying_load_events_mode: Cell::new(false), opener, creator_url: creator.url, creator_origin: creator.origin, script_window_proxies: ScriptThread::window_proxies(), } } #[expect(unsafe_code)] #[expect(clippy::too_many_arguments)] pub(crate) fn new( cx: &mut JSContext, window: &Window, browsing_context_id: BrowsingContextId, webview_id: WebViewId, frame_element: Option<&Element>, parent: Option<&WindowProxy>, opener: Option<BrowsingContextId>, creator: CreatorBrowsingContextInfo, ) -> DomRoot<WindowProxy> { unsafe { let handler = window.windowproxy_handler(); let window_jsobject = window.reflector().get_jsobject(); assert!(!window_jsobject.get().is_null()); assert_ne!( ((*get_object_class(window_jsobject.get())).flags & JSCLASS_IS_GLOBAL), 0 ); let mut realm = AutoRealm::new_from_handle(cx, window_jsobject); let cx = &mut realm; // Create a new window proxy. rooted!(&in(cx) let js_proxy = handler.new_window_proxy(cx, window_jsobject)); assert!(!js_proxy.is_null()); // Create a new browsing context. let current = Some(window.upcast::<GlobalScope>().pipeline_id()); let window_proxy = Box::new(WindowProxy::new_inherited( browsing_context_id, webview_id, current, frame_element, parent, opener, creator, )); // The window proxy owns the browsing context. // When we finalize the window proxy, it drops the browsing context it owns. SetProxyReservedSlot( js_proxy.get(), 0, &PrivateValue(&raw const (*window_proxy) as *const libc::c_void), ); // Notify the JS engine about the new window proxy binding. SetWindowProxy(cx, window_jsobject, js_proxy.handle()); // Set the reflector. debug!( "Initializing reflector of {:p} to {:p}.", window_proxy, js_proxy.get() ); window_proxy .reflector .init_reflector::<WindowProxy>(js_proxy.get()); DomRoot::from_ref(&*Box::into_raw(window_proxy)) } } #[expect(unsafe_code)] pub(crate) fn new_dissimilar_origin( cx: &mut JSContext, global_to_clone_from: &GlobalScope, browsing_context_id: BrowsingContextId, webview_id: WebViewId, parent: Option<&WindowProxy>, opener: Option<BrowsingContextId>, creator: CreatorBrowsingContextInfo, ) -> DomRoot<WindowProxy> { unsafe { let handler = WindowProxyHandler::x_origin_proxy_handler(); // Create a new browsing context. let window_proxy = Box::new(WindowProxy::new_inherited( browsing_context_id, webview_id, None, None, parent, opener, creator, )); // Create a new dissimilar-origin window. let window = DissimilarOriginWindow::new(cx, global_to_clone_from, &window_proxy); let window_jsobject = window.reflector().get_jsobject(); assert!(!window_jsobject.get().is_null()); assert_ne!( ((*get_object_class(window_jsobject.get())).flags & JSCLASS_IS_GLOBAL), 0 ); let mut realm = AutoRealm::new_from_handle(cx, window_jsobject); let cx = &mut realm; // Create a new window proxy. rooted!(&in(cx) let js_proxy = handler.new_window_proxy(cx, window_jsobject)); assert!(!js_proxy.is_null()); // The window proxy owns the browsing context. // When we finalize the window proxy, it drops the browsing context it owns. SetProxyReservedSlot( js_proxy.get(), 0, &PrivateValue(&raw const (*window_proxy) as *const libc::c_void), ); // Notify the JS engine about the new window proxy binding. SetWindowProxy(cx, window_jsobject, js_proxy.handle()); // Set the reflector. debug!( "Initializing reflector of {:p} to {:p}.", window_proxy, js_proxy.get() ); window_proxy .reflector .init_reflector::<WindowProxy>(js_proxy.get()); DomRoot::from_ref(&*Box::into_raw(window_proxy)) } } /// <https://html.spec.whatwg.org/multipage/#auxiliary-browsing-context> fn create_auxiliary_browsing_context( &self, cx: &mut JSContext, name: DOMString, noopener: bool, ) -> Option<DomRoot<WindowProxy>> { let (response_sender, response_receiver) = generic_channel::channel().unwrap(); let window = self .currently_active .get() .and_then(ScriptThread::find_document) .map(|doc| DomRoot::from_ref(doc.window())) .unwrap(); let document = self .currently_active .get() .and_then(ScriptThread::find_document) .expect("A WindowProxy creating an auxiliary to have an active document"); // <https://html.spec.whatwg.org/multipage/#navigable-target-names> // > If the user agent has been configured such that in this instance it // > will create a new top-level traversable // > // Step 9. If sandboxingFlagSet's sandbox propagates to auxiliary browsing // contexts flag is set, then all the flags that are set in sandboxingFlagSet // must be set in chosen's active browsing context's popup sandboxing flag set. let sandboxing_flag_set = document.active_sandboxing_flag_set(); let propagate_sandbox = sandboxing_flag_set .contains(SandboxingFlagSet::SANDBOX_PROPOGATES_TO_AUXILIARY_BROWSING_CONTEXTS_FLAG); let sandboxing_flag_set = if propagate_sandbox { sandboxing_flag_set } else { SandboxingFlagSet::empty() }; let blank_url = ServoUrl::parse("about:blank").ok().unwrap(); let mut load_data = LoadData::new( LoadOrigin::Script(document.origin().snapshot()), blank_url, Some(document.base_url()), // This has the effect of ensuring that the new `about:blank` URL has the // same origin as the `Document` that is creating the new browsing context. Some(window.pipeline_id()), document.global().get_referrer(), document.get_referrer_policy(), None, // Doesn't inherit secure context None, false, sandboxing_flag_set, ); load_data.is_initial_about_blank = true; let load_info = AuxiliaryWebViewCreationRequest { load_data: load_data.clone(), opener_webview_id: window.webview_id(), opener_pipeline_id: self.currently_active.get().unwrap(), response_sender, }; let constellation_msg = ScriptToConstellationMessage::CreateAuxiliaryWebView(load_info); window.send_to_constellation(constellation_msg); let response = response_receiver.recv().unwrap()?; let new_browsing_context_id = BrowsingContextId::from(response.new_webview_id); let new_pipeline_info = NewPipelineInfo { parent_info: None, new_pipeline_id: response.new_pipeline_id, browsing_context_id: new_browsing_context_id, webview_id: response.new_webview_id, opener: Some(self.browsing_context_id), load_data, viewport_details: window.viewport_details(), user_content_manager_id: response.user_content_manager_id, // Use the current `WebView`'s theme initially, but the embedder may // change this later. theme: window.theme(), target_snapshot_params: TargetSnapshotParams { sandboxing_flags: sandboxing_flag_set, iframe_element_referrer_policy: ReferrerPolicy::EmptyString, }, }; with_script_thread(|script_thread| { script_thread.spawn_pipeline(cx, new_pipeline_info); }); let new_window_proxy = ScriptThread::find_document(response.new_pipeline_id) .and_then(|doc| doc.browsing_context())?; if name.to_lowercase() != "_blank" { new_window_proxy.set_name(name); } if noopener { new_window_proxy.disown(); } else { // After creating a new auxiliary browsing context and document, // the session storage is copied over. // See https://html.spec.whatwg.org/multipage/#the-sessionstorage-attribute let (sender, receiver) = generic_channel::channel().unwrap(); let msg = WebStorageThreadMsg::Clone { sender, src: window.window_proxy().webview_id(), dest: response.new_webview_id, }; GenericSend::send(document.global().storage_threads(), msg).unwrap(); receiver.recv().unwrap(); } Some(new_window_proxy) } /// <https://html.spec.whatwg.org/multipage/#delaying-load-events-mode> pub(crate) fn is_delaying_load_events_mode(&self) -> bool { self.delaying_load_events_mode.get() } /// <https://html.spec.whatwg.org/multipage/#delaying-load-events-mode> pub(crate) fn start_delaying_load_events_mode(&self) { self.delaying_load_events_mode.set(true); } /// <https://html.spec.whatwg.org/multipage/#delaying-load-events-mode> pub(crate) fn stop_delaying_load_events_mode(&self) { self.delaying_load_events_mode.set(false); if let Some(document) = self.document() && !document.loader().events_inhibited() { ScriptThread::mark_document_with_no_blocked_loads(&document); } } // https://html.spec.whatwg.org/multipage/#disowned-its-opener pub(crate) fn disown(&self) { self.disowned.set(true); } /// <https://html.spec.whatwg.org/multipage/#dom-window-close> /// Step 3.1, set BCs `is_closing` to true. pub(crate) fn close(&self) { self.is_closing.set(true); } /// <https://html.spec.whatwg.org/multipage/#is-closing> pub(crate) fn is_closing(&self) -> bool { self.is_closing.get() } // https://html.spec.whatwg.org/multipage/#dom-opener pub(crate) fn opener(&self, cx: &mut CurrentRealm, mut retval: MutableHandleValue) { if self.disowned.get() { return retval.set(NullValue()); } let opener_id = match self.opener { Some(opener_browsing_context_id) => opener_browsing_context_id, None => return retval.set(NullValue()), }; let parent_browsing_context = self.parent.as_deref(); let opener_proxy = match self.script_window_proxies.find_window_proxy(opener_id) { Some(window_proxy) => window_proxy, None => { let sender_pipeline_id = self.currently_active().unwrap(); match ScriptThread::get_top_level_for_browsing_context( self.webview_id(), sender_pipeline_id, opener_id, ) { Some(opener_top_id) => { let global_to_clone_from = GlobalScope::from_current_realm(cx); let creator = CreatorBrowsingContextInfo::from(parent_browsing_context, None); WindowProxy::new_dissimilar_origin( cx, &global_to_clone_from, opener_id, opener_top_id, None, None, creator, ) }, None => return retval.set(NullValue()), } }, }; if opener_proxy.is_browsing_context_discarded() { return retval.set(NullValue()); } opener_proxy.safe_to_jsval(cx, retval); } // https://html.spec.whatwg.org/multipage/#window-open-steps pub(crate) fn open( &self, cx: &mut JSContext, url: USVString, target: DOMString, features: DOMString, ) -> Fallible<Option<DomRoot<WindowProxy>>> { // Note: this does not map to the spec, // but it does prevent a panic at the constellation because the browsing context // has already been discarded. // See issue: #39716 for the original problem, // and https://github.com/whatwg/html/issues/11797 for a discussion at the level of the spec. if self.discarded.get() { return Ok(None); } // Step 2. Let sourceDocument be the entry global object's associated Document. // // It's possible to end up in a situation where JS code is executing but // we have not had a chance to push an entry global (e.g. via WASM instantiation). // If that happens, fall back to the active document of this browsing context. let source_document = maybe_entry_global() .map(|global| global.as_window().Document()) .or_else(|| self.document()) .expect("Must have an entry global or active document"); // Step 4. If url is not the empty string: let url_record = if !url.is_empty() { // Step 4.1. Set urlRecord to the result of encoding-parsing a URL given url, relative to sourceDocument. let Ok(url) = source_document.encoding_parse_a_url(&url) else { // Step 4.2. If urlRecord is failure, then throw a "SyntaxError" DOMException. return Err(Error::Syntax(Some(format!( "Error parsing URL '{url}' relative to '{}'", source_document.url() )))); }; Some(url) } else { // Step 3. Let urlRecord be null. None }; // Step 5. If target is the empty string, then set target to "_blank". let non_empty_target = if target.is_empty() { DOMString::from("_blank") } else { target }; // Step 6. Let tokenizedFeatures be the result of tokenizing features. let tokenized_features = tokenize_open_features(features); // Step 7 - 8. // If tokenizedFeatures["noreferrer"] exists, then set noreferrer to // the result of parsing tokenizedFeatures["noreferrer"] as a boolean feature. let noreferrer = parse_open_feature_boolean(&tokenized_features, "noreferrer"); // Step 9. Let noopener be the result of getting noopener for window // open with sourceDocument, tokenizedFeatures, and urlRecord. let noopener = if noreferrer { true } else { parse_open_feature_boolean(&tokenized_features, "noopener") }; // (TODO) Step 10. Remove tokenizedFeatures["noopener"] and tokenizedFeatures["noreferrer"]. // (TODO) Step 11. Let referrerPolicy be the empty string. // (TODO) Step 12. If noreferrer is true, then set referrerPolicy to "no-referrer". // Step 13 - 14 // Let targetNavigable and windowType be the result of applying the rules for // choosing a navigable given target, sourceDocument's node navigable, and noopener. // If targetNavigable is null, then return null. let (chosen, new) = match self.choose_browsing_context(cx, non_empty_target, noopener) { (Some(chosen), new) => (chosen, new), (None, _) => return Ok(None), }; // TODO Step 15.2, Set up browsing context features for targetNavigable's // active browsing context given tokenizedFeatures. let target_document = match chosen.document() { Some(target_document) => target_document, None => return Ok(None), }; let has_trustworthy_ancestor_origin = if new { target_document.has_trustworthy_ancestor_or_current_origin() } else { false }; let target_window = target_document.window(); // Step 15.3. If urlRecord is null, then set urlRecord to a URL record representing about:blank. let url_record = url_record.unwrap_or(ServoUrl::parse("about:blank").unwrap()); // Step 15.4. If urlRecord matches about:blank, then perform the URL and history update steps given targetNavigable's active document and urlRecord. // // This happened in the constellation as part of creating the auxiliary browsing context. if !url_record.matches_about_blank() { let referrer = if noreferrer { Referrer::NoReferrer } else { target_window.as_global_scope().get_referrer() }; // Propagate CSP list and about-base-url from opener to new document let csp_list = source_document.get_csp_list().clone(); target_document.set_csp_list(csp_list); // Step 15.5 Otherwise, navigate targetNavigable to urlRecord using sourceDocument, // with referrerPolicy set to referrerPolicy and exceptionsEnabled set to true. // FIXME: referrerPolicy may not be used properly here. exceptionsEnabled not used. let mut load_data = LoadData::new( LoadOrigin::Script(source_document.origin().snapshot()), url_record, target_document.about_base_url(), Some(target_window.pipeline_id()), referrer, target_document.get_referrer_policy(), Some(target_window.as_global_scope().is_secure_context()), Some(target_document.insecure_requests_policy()), has_trustworthy_ancestor_origin, target_document.creation_sandboxing_flag_set_considering_parent_iframe(), ); // Handle javascript: URLs specially to report CSP violations to the source window // https://html.spec.whatwg.org/multipage/#navigate-to-a-javascript:-url if load_data.url.scheme() == "javascript" { let existing_global = source_document.global(); // Check CSP and report violations to the source (existing) window if !ScriptThread::can_navigate_to_javascript_url( cx, &existing_global, target_window.as_global_scope(), &mut load_data, None, ) { // CSP blocked the navigation, don't proceed return Ok(target_document.browsing_context()); } } let history_handling = if new { NavigationHistoryBehavior::Replace } else { NavigationHistoryBehavior::Push }; navigate(cx, target_window, history_handling, false, load_data); } // Step 17 (Dis-owning has been done in create_auxiliary_browsing_context). if noopener { return Ok(None); } // Step 18 Ok(target_document.browsing_context()) } // https://html.spec.whatwg.org/multipage/#the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name pub(crate) fn choose_browsing_context( &self, cx: &mut JSContext, name: DOMString, noopener: bool, ) -> (Option<DomRoot<WindowProxy>>, bool) { match name.to_lowercase().as_ref() { "" | "_self" => { // Step 3. (Some(DomRoot::from_ref(self)), false) }, "_parent" => { // Step 4 if let Some(parent) = self.parent() { return (Some(DomRoot::from_ref(parent)), false); } (None, false) }, "_top" => { // Step 5 (Some(DomRoot::from_ref(self.top())), false) }, "_blank" => ( self.create_auxiliary_browsing_context(cx, name, noopener), true, ), _ => { // Step 6. // TODO: expand the search to all 'familiar' bc, // including auxiliaries familiar by way of their opener. // See https://html.spec.whatwg.org/multipage/#familiar-with match ScriptThread::find_window_proxy_by_name(&name) { Some(proxy) => (Some(proxy), false), None => ( self.create_auxiliary_browsing_context(cx, name, noopener), true, ), } }, } } pub(crate) fn is_auxiliary(&self) -> bool { self.opener.is_some() } pub(crate) fn discard_browsing_context(&self) { self.discarded.set(true); } pub(crate) fn is_browsing_context_discarded(&self) -> bool { self.discarded.get() } pub(crate) fn browsing_context_id(&self) -> BrowsingContextId { self.browsing_context_id } pub(crate) fn webview_id(&self) -> WebViewId { self.webview_id } /// If the containing `<iframe>` of this [`WindowProxy`] is from a same-origin page, /// this will return an [`Element`] of the `<iframe>` element in the realm of the parent /// page. pub(crate) fn frame_element(&self) -> Option<&Element> { self.frame_element.as_deref() } pub(crate) fn document(&self) -> Option<DomRoot<Document>> { self.currently_active .get() .and_then(ScriptThread::find_document) } pub(crate) fn parent(&self) -> Option<&WindowProxy> { self.parent.as_deref() } pub(crate) fn top(&self) -> &WindowProxy { let mut result = self; while let Some(parent) = result.parent() { result = parent; } result } pub fn document_origin(&self) -> Option<String> { let pipeline_id = self.currently_active()?; let (result_sender, result_receiver) = generic_channel::channel().unwrap(); self.global() .script_to_constellation_chan() .send(ScriptToConstellationMessage::GetDocumentOrigin( pipeline_id, result_sender, )) .ok()?; result_receiver.recv().ok()? } #[expect(unsafe_code)] /// Change the Window that this WindowProxy resolves to. // TODO: support setting the window proxy to a dummy value, // to handle the case when the active document is in another script thread. fn set_window(&self, cx: &mut JSContext, window: &GlobalScope, handler: &WindowProxyHandler) { unsafe { debug!("Setting window of {:p}.", self); let window_jsobject = window.reflector().get_jsobject(); let old_js_proxy = self.reflector.get_jsobject(); assert!(!window_jsobject.get().is_null()); assert_ne!( ((*get_object_class(window_jsobject.get())).flags & JSCLASS_IS_GLOBAL), 0 ); let mut realm = AutoRealm::new_from_handle(cx, window_jsobject); let cx = &mut realm; // The old window proxy no longer owns this browsing context. SetProxyReservedSlot(old_js_proxy.get(), 0, &PrivateValue(ptr::null_mut())); // Brain transplant the window proxy. Brain transplantation is // usually done to move a window proxy between compartments, but // that's not what we are doing here. We need to do this just // because we want to replace the wrapper's `ProxyTraps`, but we // don't want to update its identity. rooted!(&in(cx) let new_js_proxy = handler.new_window_proxy(cx, window_jsobject)); // Explicitly set this slot to a null pointer in case a GC occurs before we // are ready to set it to a real value. SetProxyReservedSlot(new_js_proxy.get(), 0, &PrivateValue(ptr::null_mut())); debug!( "Transplanting proxy from {:p} to {:p}.", old_js_proxy.get(), new_js_proxy.get() ); rooted!(&in(cx) let new_js_proxy = JS_TransplantObject(cx, old_js_proxy, new_js_proxy.handle())); debug!("Transplanted proxy is {:p}.", new_js_proxy.get()); // Transfer ownership of this browsing context from the old window proxy to the new one. SetProxyReservedSlot( new_js_proxy.get(), 0, &PrivateValue(self as *const _ as *const libc::c_void), ); // Notify the JS engine about the new window proxy binding. SetWindowProxy(cx, window_jsobject, new_js_proxy.handle()); // Update the reflector. debug!( "Setting reflector of {:p} to {:p}.", self, new_js_proxy.get() ); self.reflector.rootable().set(new_js_proxy.get()); } } pub(crate) fn set_currently_active(&self, cx: &mut JSContext, window: &Window) { if let Some(pipeline_id) = self.currently_active() && pipeline_id == window.pipeline_id() { return debug!( "Attempt to set the currently active window to the currently active window." ); } let global_scope = window.as_global_scope(); self.set_window(cx, global_scope, WindowProxyHandler::proxy_handler()); self.currently_active.set(Some(global_scope.pipeline_id())); } pub(crate) fn unset_currently_active(&self, cx: &mut JSContext) { if self.currently_active().is_none() { return debug!( "Attempt to unset the currently active window on a windowproxy that does not have one." ); } let globalscope = self.global(); let window = DissimilarOriginWindow::new(cx, &globalscope, self); self.set_window( cx, window.upcast(), WindowProxyHandler::x_origin_proxy_handler(), ); self.currently_active.set(None); } pub(crate) fn currently_active(&self) -> Option<PipelineId> { self.currently_active.get() } pub(crate) fn get_name(&self) -> DOMString { self.name.borrow().clone() } pub(crate) fn set_name(&self, name: DOMString) { *self.name.borrow_mut() = name; } } /// A browsing context can have a creator browsing context, the browsing context that /// was responsible for its creation. If a browsing context has a parent browsing context, /// then that is its creator browsing context. Otherwise, if the browsing context has an /// opener browsing context, then that is its creator browsing context. Otherwise, the /// browsing context has no creator browsing context. /// /// If a browsing context A has a creator browsing context, then the Document that was the /// active document of that creator browsing context at the time A was created is the creator /// Document. /// /// See: <https://html.spec.whatwg.org/multipage/#creating-browsing-contexts> #[derive(Debug, Deserialize, Serialize)] pub(crate) struct CreatorBrowsingContextInfo { /// Creator document URL. url: Option<ServoUrl>, /// Creator document origin. origin: Option<ImmutableOrigin>, } impl CreatorBrowsingContextInfo { pub(crate) fn from( parent: Option<&WindowProxy>, opener: Option<&WindowProxy>, ) -> CreatorBrowsingContextInfo { let creator = match (parent, opener) { (Some(parent), _) => parent.document(), (None, Some(opener)) => opener.document(), (None, None) => None, }; let url = creator.as_deref().map(|document| document.url()); let origin = creator .as_deref() .map(|document| document.origin().immutable().clone()); CreatorBrowsingContextInfo { url, origin } } } /// <https://html.spec.whatwg.org/multipage/#concept-window-open-features-tokenize> fn tokenize_open_features(features: DOMString) -> IndexMap<String, String> { let is_feature_sep = |c: char| c.is_ascii_whitespace() || ['=', ','].contains(&c); // Step 1 let mut tokenized_features = IndexMap::new(); // Step 2 let features = features.str(); let mut iter = features.chars(); let mut cur = iter.next(); // Step 3 while cur.is_some() { // Step 3.1 & 3.2 let mut name = String::new(); let mut value = String::new(); // Step 3.3 while let Some(cur_char) = cur { if !is_feature_sep(cur_char) { break; } cur = iter.next(); } // Step 3.4 while let Some(cur_char) = cur { if is_feature_sep(cur_char) { break; } name.push(cur_char.to_ascii_lowercase()); cur = iter.next(); } // Step 3.5 let normalized_name = String::from(match name.as_ref() { "screenx" => "left", "screeny" => "top", "innerwidth" => "width", "innerheight" => "height", _ => name.as_ref(), }); // Step 3.6 while let Some(cur_char) = cur { if cur_char == '=' || cur_char == ',' || !is_feature_sep(cur_char) { break; } cur = iter.next(); } // Step 3.7 if cur.is_some() && is_feature_sep(cur.unwrap()) { // Step 3.7.1 while let Some(cur_char) = cur { if !is_feature_sep(cur_char) || cur_char == ',' { break; } cur = iter.next(); } // Step 3.7.2 while let Some(cur_char) = cur { if is_feature_sep(cur_char) { break; } value.push(cur_char.to_ascii_lowercase()); cur = iter.next(); } } // Step 3.8 if !name.is_empty() { tokenized_features.insert(normalized_name, value); } } // Step 4 tokenized_features } /// <https://html.spec.whatwg.org/multipage/#concept-window-open-features-parse-boolean> fn parse_open_feature_boolean(tokenized_features: &IndexMap<String, String>, name: &str) -> bool { if let Some(value) = tokenized_features.get(name) { // Step 1 & 2 if value.is_empty() || value == "yes" { return true; } // Step 3 & 4 if let Ok(int) = parse_integer(value.chars()) { return int != 0; } } // Step 5 false } // This is only called from extern functions, // there's no use using the lifetimed handles here. // https://html.spec.whatwg.org/multipage/#accessing-other-browsing-contexts #[expect(unsafe_code)] #[expect(non_snake_case)] unsafe fn GetSubframeWindowProxy( cx: &mut JSContext, proxy: RawHandleObject, id: RawHandleId, ) -> Option<(DomRoot<WindowProxy>, u32)> { let index = get_array_index_from_id(unsafe { Handle::from_raw(id) }); if let Some(index) = index { let mut slot = UndefinedValue(); unsafe { GetProxyPrivate(*proxy, &mut slot) }; rooted!(&in(cx) let target = slot.to_object()); let script_window_proxies = ScriptThread::window_proxies(); if let Ok(win) = root_from_handleobject::<Window>(cx, target.handle()) { let browsing_context_id = win.window_proxy().browsing_context_id(); let (result_sender, result_receiver) = generic_channel::channel().unwrap(); let _ = win.as_global_scope().script_to_constellation_chan().send( ScriptToConstellationMessage::GetChildBrowsingContextId( browsing_context_id, index as usize, result_sender, ), ); return result_receiver .recv() .ok() .and_then(|maybe_bcid| maybe_bcid) .and_then(|id| script_window_proxies.find_window_proxy(id)) .map(|proxy| (proxy, (JSPROP_ENUMERATE | JSPROP_READONLY) as u32)); } else if let Ok(win) = root_from_handleobject::<DissimilarOriginWindow>(cx, target.handle()) { let browsing_context_id = win.window_proxy().browsing_context_id(); let (result_sender, result_receiver) = generic_channel::channel().unwrap(); let _ = win.global().script_to_constellation_chan().send( ScriptToConstellationMessage::GetChildBrowsingContextId( browsing_context_id, index as usize, result_sender, ), ); return result_receiver .recv() .ok() .and_then(|maybe_bcid| maybe_bcid) .and_then(|id| script_window_proxies.find_window_proxy(id)) .map(|proxy| (proxy, JSPROP_READONLY as u32)); } } None } #[expect(unsafe_code)] unsafe extern "C" fn get_own_property_descriptor( cx: *mut RawJSContext, proxy: RawHandleObject, id: RawHandleId, desc: RawMutableHandle<PropertyDescriptor>, is_none: *mut bool, ) -> bool { let mut cx = unsafe { // SAFETY: We are in SM hook JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook")) }; let cx = &mut cx; let window = unsafe { GetSubframeWindowProxy(cx, proxy, id) }; let desc = unsafe { MutableHandle::from_raw(desc) }; if let Some((window, attrs)) = window { rooted!(&in(cx) let mut val = UndefinedValue()); window.safe_to_jsval(cx, val.handle_mut()); set_property_descriptor(desc, val.handle(), attrs, unsafe { &mut *is_none }); return true; } let mut slot = UndefinedValue(); unsafe { GetProxyPrivate(proxy.get(), &mut slot) }; rooted!(&in(cx) let target = slot.to_object()); unsafe { JS_GetOwnPropertyDescriptorById(cx, target.handle(), Handle::from_raw(id), desc, is_none) } } #[expect(unsafe_code)] unsafe extern "C" fn define_property( cx: *mut RawJSContext, proxy: RawHandleObject, id: RawHandleId, desc: RawHandle<PropertyDescriptor>, res: *mut ObjectOpResult, ) -> bool { if get_array_index_from_id(unsafe { Handle::from_raw(id) }).is_some() { // Spec says to Reject whether this is a supported index or not, // since we have no indexed setter or indexed creator. That means // throwing in strict mode (FIXME: Bug 828137), doing nothing in // non-strict mode. unsafe { (*res).code_ = JSErrNum::JSMSG_CANT_DEFINE_WINDOW_ELEMENT as ::libc::uintptr_t; } return true; } let mut slot = UndefinedValue(); unsafe { GetProxyPrivate(*proxy.ptr, &mut slot) }; rooted!(in(cx) let target = slot.to_object()); unsafe { JS_DefinePropertyById(cx, target.handle().into(), id, desc, res) } } #[expect(unsafe_code)] unsafe extern "C" fn has( cx: *mut RawJSContext, proxy: RawHandleObject, id: RawHandleId, bp: *mut bool, ) -> bool { let mut cx = unsafe { // SAFETY: We are in SM hook JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook")) }; let cx = &mut cx; let window = unsafe { GetSubframeWindowProxy(cx, proxy, id) }; if window.is_some() { unsafe { *bp = true }; return true; } let mut slot = UndefinedValue(); unsafe { GetProxyPrivate(*proxy.ptr, &mut slot) }; rooted!(&in(cx) let target = slot.to_object()); let mut found = false; if !unsafe { JS_HasPropertyById(cx, target.handle(), Handle::from_raw(id), &mut found) } { return false; } unsafe { *bp = found }; true } #[expect(unsafe_code)] unsafe extern "C" fn get( cx: *mut RawJSContext, proxy: RawHandleObject, receiver: RawHandleValue, id: RawHandleId, vp: RawMutableHandleValue, ) -> bool { let mut cx = unsafe { // SAFETY: We are in SM hook JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook")) }; let cx = &mut cx; let window = unsafe { GetSubframeWindowProxy(cx, proxy, id) }; let vp = unsafe { MutableHandle::from_raw(vp) }; if let Some((window, _attrs)) = window { window.safe_to_jsval(cx, vp); return true; } let mut slot = UndefinedValue(); unsafe { GetProxyPrivate(*proxy.ptr, &mut slot) }; rooted!(&in(cx) let target = slot.to_object()); unsafe { JS_ForwardGetPropertyTo( cx, target.handle(), Handle::from_raw(id), Handle::from_raw(receiver), vp, ) } } #[expect(unsafe_code)] unsafe extern "C" fn set( cx: *mut RawJSContext, proxy: RawHandleObject, id: RawHandleId, v: RawHandleValue, receiver: RawHandleValue, res: *mut ObjectOpResult, ) -> bool { if get_array_index_from_id(unsafe { Handle::from_raw(id) }).is_some() { // Reject (which means throw if and only if strict) the set. unsafe { (*res).code_ = JSErrNum::JSMSG_READ_ONLY as ::libc::uintptr_t }; return true; } let mut slot = UndefinedValue(); unsafe { GetProxyPrivate(*proxy.ptr, &mut slot) }; rooted!(in(cx) let target = slot.to_object()); unsafe { JS_ForwardSetPropertyTo(cx, target.handle().into(), id, v, receiver, res) } } #[expect(unsafe_code)] unsafe extern "C" fn get_prototype_if_ordinary( _: *mut RawJSContext, _: RawHandleObject, is_ordinary: *mut bool, _: RawMutableHandleObject, ) -> bool { // Window's [[GetPrototypeOf]] trap isn't the ordinary definition: // // https://html.spec.whatwg.org/multipage/#windowproxy-getprototypeof // // We nonetheless can implement it with a static [[Prototype]], because // wrapper-class handlers (particularly, XOW in FilteringWrapper.cpp) supply // all non-ordinary behavior. // // But from a spec point of view, it's the exact same object in both cases -- // only the observer's changed. So this getPrototypeIfOrdinary trap on the // non-wrapper object *must* report non-ordinary, even if static [[Prototype]] // usually means ordinary. unsafe { *is_ordinary = false }; true } static PROXY_TRAPS: ProxyTraps = ProxyTraps { // TODO: These traps should change their behavior depending on // `IsPlatformObjectSameOrigin(this.[[Window]])` enter: None, getOwnPropertyDescriptor: Some(get_own_property_descriptor), defineProperty: Some(define_property), ownPropertyKeys: None, delete_: None, enumerate: None, getPrototypeIfOrdinary: Some(get_prototype_if_ordinary), getPrototype: None, // TODO: return `null` if cross origin-domain setPrototype: None, setImmutablePrototype: None, preventExtensions: None, isExtensible: None, has: Some(has), get: Some(get), set: Some(set), call: None, construct: None, hasOwn: None, getOwnEnumerablePropertyKeys: None, nativeCall: None, objectClassIs: None, className: None, fun_toString: None, boxedValue_unbox: None, defaultValue: None, trace: Some(trace), finalize: Some(finalize), objectMoved: None, isCallable: None, isConstructor: None, }; /// Proxy handler for a WindowProxy. /// Has ownership of the inner pointer and deallocates it when it is no longer needed. pub(crate) struct WindowProxyHandler(*const libc::c_void); impl MallocSizeOf for WindowProxyHandler { fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { // FIXME(#6907) this is a pointer to memory allocated by `new` in NewProxyHandler in rust-mozjs. 0 } } // Safety: Send and Sync is guaranteed since the underlying pointer and all its associated methods in C++ are const. #[expect(unsafe_code)] unsafe impl Send for WindowProxyHandler {} // Safety: Send and Sync is guaranteed since the underlying pointer and all its associated methods in C++ are const. #[expect(unsafe_code)] unsafe impl Sync for WindowProxyHandler {} #[expect(unsafe_code)] impl WindowProxyHandler { fn new(traps: &ProxyTraps) -> Self { // Safety: Foreign function generated by bindgen. Pointer is freed in drop to prevent memory leak. let ptr = unsafe { CreateWrapperProxyHandler(traps) }; assert!(!ptr.is_null()); Self(ptr) } /// Returns a single, shared WindowProxyHandler that contains XORIGIN_PROXY_TRAPS. pub(crate) fn x_origin_proxy_handler() -> &'static Self { use std::sync::OnceLock; /// We are sharing a single instance for the entire programs here due to lifetime issues. /// The pointer in self.0 is known to C++ and visited by the GC. Hence, we don't know when /// it is safe to free it. /// Sharing a single instance should be fine because all methods on this pointer in C++ /// are const and don't modify its internal state. static SINGLETON: OnceLock<WindowProxyHandler> = OnceLock::new(); SINGLETON.get_or_init(|| Self::new(&XORIGIN_PROXY_TRAPS)) } /// Returns a single, shared WindowProxyHandler that contains normal PROXY_TRAPS. pub(crate) fn proxy_handler() -> &'static Self { use std::sync::OnceLock; /// We are sharing a single instance for the entire programs here due to lifetime issues. /// The pointer in self.0 is known to C++ and visited by the GC. Hence, we don't know when /// it is safe to free it. /// Sharing a single instance should be fine because all methods on this pointer in C++ /// are const and don't modify its internal state. static SINGLETON: OnceLock<WindowProxyHandler> = OnceLock::new(); SINGLETON.get_or_init(|| Self::new(&PROXY_TRAPS)) } /// Creates a new WindowProxy object on the C++ side and returns the pointer to it. /// The pointer should be owned by the GC. fn new_window_proxy( &self, cx: &mut JSContext, window_jsobject: js::gc::HandleObject, ) -> *mut JSObject { let obj = unsafe { NewWindowProxy(cx, window_jsobject, self.0) }; assert!(!obj.is_null()); obj } } #[expect(unsafe_code)] impl Drop for WindowProxyHandler { fn drop(&mut self) { // Safety: Pointer is allocated by corresponding C++ function, owned by this // struct and not accessible from outside. unsafe { DeleteWrapperProxyHandler(self.0); } } } // The proxy traps for cross-origin windows. // These traps often throw security errors, and only pass on calls to methods // defined in the DissimilarOriginWindow IDL. // TODO: reuse the infrastructure in `proxyhandler.rs`. For starters, the calls // to this function should be replaced with those to // `report_cross_origin_denial`. #[expect(unsafe_code)] fn throw_security_error(realm: &mut CurrentRealm) -> bool { if !unsafe { JS_IsExceptionPending(realm) } { let global = GlobalScope::from_current_realm(realm); throw_dom_exception(realm, &global, Error::Security(None)); } false } #[expect(unsafe_code)] unsafe extern "C" fn has_xorigin( cx: *mut RawJSContext, proxy: RawHandleObject, id: RawHandleId, bp: *mut bool, ) -> bool { let mut cx = unsafe { // SAFETY: We are in SM hook JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook")) }; let mut slot = UndefinedValue(); unsafe { GetProxyPrivate(*proxy.ptr, &mut slot) }; rooted!(&in(cx) let target = slot.to_object()); let mut found = false; unsafe { JS_HasOwnPropertyById(&mut cx, target.handle(), Handle::from_raw(id), &mut found) }; if found { unsafe { *bp = true }; true } else { let mut realm = CurrentRealm::assert(&mut cx); throw_security_error(&mut realm) } } #[expect(unsafe_code)] unsafe extern "C" fn get_xorigin( cx: *mut RawJSContext, proxy: RawHandleObject, receiver: RawHandleValue, id: RawHandleId, vp: RawMutableHandleValue, ) -> bool { let mut found = false; unsafe { has_xorigin(cx, proxy, id, &mut found) }; found && unsafe { get(cx, proxy, receiver, id, vp) } } #[expect(unsafe_code)] unsafe extern "C" fn set_xorigin( cx: *mut RawJSContext, _: RawHandleObject, _: RawHandleId, _: RawHandleValue, _: RawHandleValue, _: *mut ObjectOpResult, ) -> bool { let mut cx = unsafe { // SAFETY: We are in SM hook JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook")) }; let mut realm = CurrentRealm::assert(&mut cx); throw_security_error(&mut realm) } #[expect(unsafe_code)] unsafe extern "C" fn delete_xorigin( cx: *mut RawJSContext, _: RawHandleObject, _: RawHandleId, _: *mut ObjectOpResult, ) -> bool { let mut cx = unsafe { // SAFETY: We are in SM hook JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook")) }; let mut realm = CurrentRealm::assert(&mut cx); throw_security_error(&mut realm) } #[expect(unsafe_code)] #[expect(non_snake_case)] unsafe extern "C" fn getOwnPropertyDescriptor_xorigin( cx: *mut RawJSContext, proxy: RawHandleObject, id: RawHandleId, desc: RawMutableHandle<PropertyDescriptor>, is_none: *mut bool, ) -> bool { let mut found = false; unsafe { has_xorigin(cx, proxy, id, &mut found) }; found && unsafe { get_own_property_descriptor(cx, proxy, id, desc, is_none) } } #[expect(unsafe_code)] #[expect(non_snake_case)] unsafe extern "C" fn defineProperty_xorigin( cx: *mut RawJSContext, _: RawHandleObject, _: RawHandleId, _: RawHandle<PropertyDescriptor>, _: *mut ObjectOpResult, ) -> bool { let mut cx = unsafe { // SAFETY: We are in SM hook JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook")) }; let mut realm = CurrentRealm::assert(&mut cx); throw_security_error(&mut realm) } #[expect(unsafe_code)] #[expect(non_snake_case)] unsafe extern "C" fn preventExtensions_xorigin( cx: *mut RawJSContext, _: RawHandleObject, _: *mut ObjectOpResult, ) -> bool { let mut cx = unsafe { // SAFETY: We are in SM hook JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook")) }; let mut realm = CurrentRealm::assert(&mut cx); throw_security_error(&mut realm) } static XORIGIN_PROXY_TRAPS: ProxyTraps = ProxyTraps { enter: None, getOwnPropertyDescriptor: Some(getOwnPropertyDescriptor_xorigin), defineProperty: Some(defineProperty_xorigin), ownPropertyKeys: None, delete_: Some(delete_xorigin), enumerate: None, getPrototypeIfOrdinary: None, getPrototype: None, setPrototype: None, setImmutablePrototype: None, preventExtensions: Some(preventExtensions_xorigin), isExtensible: None, has: Some(has_xorigin), get: Some(get_xorigin), set: Some(set_xorigin), call: None, construct: None, hasOwn: Some(has_xorigin), getOwnEnumerablePropertyKeys: None, nativeCall: None, objectClassIs: None, className: None, fun_toString: None, boxedValue_unbox: None, defaultValue: None, trace: Some(trace), finalize: Some(finalize), objectMoved: None, isCallable: None, isConstructor: None, }; // How WindowProxy objects are garbage collected. #[expect(unsafe_code)] unsafe extern "C" fn finalize(_fop: *mut GCContext, obj: *mut JSObject) { let mut slot = UndefinedValue(); unsafe { GetProxyReservedSlot(obj, 0, &mut slot) }; let this = slot.to_private() as *mut WindowProxy; if this.is_null() { // GC during obj creation or after transplanting. return; } unsafe { (*this).reflector.drop_memory(&*this); let jsobject = (*this).reflector.get_jsobject().get(); debug!( "WindowProxy finalize: {:p}, with reflector {:p} from {:p}.", this, jsobject, obj ); let _ = Box::from_raw(this); } } #[expect(unsafe_code)] unsafe extern "C" fn trace(trc: *mut JSTracer, obj: *mut JSObject) { let mut slot = UndefinedValue(); unsafe { GetProxyReservedSlot(obj, 0, &mut slot) }; let this = slot.to_private() as *const WindowProxy; if this.is_null() { // GC during obj creation or after transplanting. return; } unsafe { (*this).trace(trc) }; }