Skip to main content

script/dom/window/
windowproxy.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::ptr::{self, NonNull};
7use std::rc::Rc;
8
9use content_security_policy::sandboxing_directive::SandboxingFlagSet;
10use dom_struct::dom_struct;
11use html5ever::local_name;
12use indexmap::map::IndexMap;
13use itertools::Either;
14use js::JSCLASS_IS_GLOBAL;
15use js::context::JSContext;
16use js::gc::{HandleId, HandleObject, HandleValue, MutableHandleObject};
17use js::glue::{
18    CreateWrapperProxyHandler, DeleteWrapperProxyHandler, GetProxyPrivate, GetProxyReservedSlot,
19    ProxyTraps, SetProxyReservedSlot,
20};
21use js::jsapi::{
22    GCContext, Handle as RawHandle, HandleId as RawHandleId, HandleObject as RawHandleObject,
23    HandleValue as RawHandleValue, JS_DefinePropertyById, JS_DeletePropertyById,
24    JS_ForwardSetPropertyTo, JSContext as RawJSContext, JSErrNum, JSITER_HIDDEN, JSITER_OWNONLY,
25    JSITER_SYMBOLS, JSObject, JSPROP_ENUMERATE, JSPROP_READONLY, JSTracer,
26    MutableHandle as RawMutableHandle, MutableHandleIdVector as RawMutableHandleIdVector,
27    MutableHandleObject as RawMutableHandleObject, MutableHandleValue as RawMutableHandleValue,
28    ObjectOpResult, PropertyDescriptor, jsid,
29};
30use js::jsval::{NullValue, PrivateValue, UndefinedValue};
31use js::realm::{AutoRealm, CurrentRealm};
32use js::rust::wrappers2::{
33    AppendToIdVector, JS_ForwardGetPropertyTo, JS_HasOwnPropertyById, JS_HasPropertyById,
34    JS_TransplantObject, NewWindowProxy, SetWindowProxy, int_to_jsid,
35};
36use js::rust::{Handle, MutableHandle, MutableHandleValue, get_object_class};
37use js::typedarray::JSObjectStorage;
38use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
39use net_traits::ReferrerPolicy;
40use net_traits::request::Referrer;
41use script_bindings::cell::DomRefCell;
42use script_bindings::codegen::GenericBindings::DissimilarOriginWindowBinding::{
43    self, DissimilarOriginWindowMethods,
44};
45use script_bindings::codegen::GenericBindings::HTMLIFrameElementBinding::HTMLIFrameElementMethods;
46use script_bindings::codegen::GenericBindings::WindowBinding::{
47    self, GetProtoObject, WindowMethods,
48};
49use script_bindings::conversions::jsid_to_string;
50use script_bindings::domstring::TracedDOMString;
51use script_bindings::js::{GetPropertyKeys, JS_GetOwnPropertyDescriptorById};
52use script_bindings::proxyhandler::{
53    self, CROSS_ORIGIN_PROPERTY_HOLDER_WEAK_MAP_SLOT, CrossOriginProperties,
54    cross_origin_get_own_property_helper, cross_origin_own_property_keys,
55    cross_origin_property_fallback, cross_origin_set, is_extensible,
56    is_platform_object_same_origin, maybe_cross_origin_get_prototype,
57    maybe_cross_origin_set_prototype_rawcx, prevent_extensions, report_cross_origin_denial,
58    set_property_descriptor,
59};
60use script_bindings::reflector::{DomObject, MutDomObject, Reflector};
61use script_traits::{NewPipelineInfo, WebViewState};
62use serde::{Deserialize, Serialize};
63use servo_base::generic_channel;
64use servo_base::generic_channel::GenericSend;
65use servo_base::id::{BrowsingContextId, PipelineId, WebViewId};
66use servo_constellation_traits::{
67    AuxiliaryWebViewCreationRequest, LoadData, LoadOrigin, NavigationHistoryBehavior,
68    ScriptToConstellationMessage, TargetSnapshotParams,
69};
70use servo_url::{ImmutableOrigin, OriginSnapshot, ServoUrl};
71use storage_traits::webstorage_thread::WebStorageThreadMsg;
72use style::attr::parse_integer;
73
74use crate::DomTypeHolder;
75use crate::dom::bindings::conversions::{ToJSValConvertible, root_from_handleobject};
76use crate::dom::bindings::error::{Error, Fallible};
77use crate::dom::bindings::inheritance::Castable;
78use crate::dom::bindings::reflector::DomGlobal;
79use crate::dom::bindings::root::{Dom, DomRoot};
80use crate::dom::bindings::settings_stack::maybe_entry_global;
81use crate::dom::bindings::str::{DOMString, USVString};
82use crate::dom::bindings::trace::JSTraceable;
83use crate::dom::bindings::utils::get_array_index_from_id;
84use crate::dom::dissimilaroriginwindow::DissimilarOriginWindow;
85use crate::dom::document::Document;
86use crate::dom::element::Element;
87use crate::dom::globalscope::GlobalScope;
88use crate::dom::node::node::NodeTraits;
89use crate::dom::window::Window;
90use crate::event_loop::script_thread::{ScriptThread, with_script_thread};
91use crate::event_loop::script_window_proxies::ScriptWindowProxies;
92use crate::navigation::navigate;
93
94#[dom_struct]
95// NOTE: the browsing context for a window is managed in two places:
96// here, in script, but also in the constellation. The constellation
97// manages the session history, which in script is accessed through
98// History objects, messaging the constellation.
99pub(crate) struct WindowProxy {
100    /// The JS WindowProxy object.
101    /// Unlike other reflectors, we mutate this field because
102    /// we have to brain-transplant the reflector when the WindowProxy
103    /// changes Window.
104    reflector: Reflector,
105
106    /// The id of the browsing context.
107    /// In the case that this is a nested browsing context, this is the id
108    /// of the container.
109    #[no_trace]
110    browsing_context_id: BrowsingContextId,
111
112    // https://html.spec.whatwg.org/multipage/#opener-browsing-context
113    #[no_trace]
114    opener: Option<BrowsingContextId>,
115
116    /// The frame id of the top-level ancestor browsing context.
117    /// In the case that this is a top-level window, this is our id.
118    #[no_trace]
119    webview_id: WebViewId,
120
121    /// The name of the browsing context (sometimes, but not always,
122    /// equal to the name of a container element)
123    name: DomRefCell<TracedDOMString>,
124    /// The pipeline id of the currently active document.
125    /// May be None, when the currently active document is in another script thread.
126    /// We do not try to keep the pipeline id for documents in other threads,
127    /// as this would require the constellation notifying many script threads about
128    /// the change, which could be expensive.
129    #[no_trace]
130    currently_active: Cell<Option<PipelineId>>,
131
132    /// Has the browsing context been discarded?
133    discarded: Cell<bool>,
134
135    /// Has the browsing context been disowned?
136    disowned: Cell<bool>,
137
138    /// <https://html.spec.whatwg.org/multipage/#is-closing>
139    is_closing: Cell<bool>,
140
141    /// If the containing `<iframe>` of this [`WindowProxy`] is from a same-origin page,
142    /// this will be the [`Element`] of the `<iframe>` element in the realm of the
143    /// parent page. Otherwise, it is `None`.
144    frame_element: Option<Dom<Element>>,
145
146    /// The parent browsing context's window proxy, if this is a nested browsing context
147    parent: Option<Dom<WindowProxy>>,
148
149    /// <https://html.spec.whatwg.org/multipage/#delaying-load-events-mode>
150    delaying_load_events_mode: Cell<bool>,
151
152    /// The creator browsing context's url.
153    #[no_trace]
154    creator_url: Option<ServoUrl>,
155
156    /// The creator browsing context's origin.
157    #[no_trace]
158    creator_origin: Option<ImmutableOrigin>,
159
160    /// The window proxies the script thread knows.
161    #[conditional_malloc_size_of]
162    script_window_proxies: Rc<ScriptWindowProxies>,
163}
164
165impl WindowProxy {
166    fn new_inherited(
167        browsing_context_id: BrowsingContextId,
168        webview_id: WebViewId,
169        currently_active: Option<PipelineId>,
170        frame_element: Option<&Element>,
171        parent: Option<&WindowProxy>,
172        opener: Option<BrowsingContextId>,
173        creator: CreatorBrowsingContextInfo,
174    ) -> WindowProxy {
175        let name = frame_element.map_or(DOMString::new(), |e| {
176            e.get_string_attribute(&local_name!("name"))
177        });
178        WindowProxy {
179            reflector: Reflector::new(),
180            browsing_context_id,
181            webview_id,
182            name: DomRefCell::new(name.traced()),
183            currently_active: Cell::new(currently_active),
184            discarded: Cell::new(false),
185            disowned: Cell::new(false),
186            is_closing: Cell::new(false),
187            frame_element: frame_element.map(Dom::from_ref),
188            parent: parent.map(Dom::from_ref),
189            delaying_load_events_mode: Cell::new(false),
190            opener,
191            creator_url: creator.url,
192            creator_origin: creator.origin,
193            script_window_proxies: ScriptThread::window_proxies(),
194        }
195    }
196
197    #[expect(unsafe_code)]
198    #[expect(clippy::too_many_arguments)]
199    pub(crate) fn new(
200        cx: &mut JSContext,
201        window: &Window,
202        browsing_context_id: BrowsingContextId,
203        webview_id: WebViewId,
204        frame_element: Option<&Element>,
205        parent: Option<&WindowProxy>,
206        opener: Option<BrowsingContextId>,
207        creator: CreatorBrowsingContextInfo,
208    ) -> DomRoot<WindowProxy> {
209        unsafe {
210            let handler = window.windowproxy_handler();
211
212            let window_jsobject = window.reflector().get_jsobject();
213            assert!(!window_jsobject.get().is_null());
214            assert_ne!(
215                ((*get_object_class(window_jsobject.get())).flags & JSCLASS_IS_GLOBAL),
216                0
217            );
218
219            let mut realm = AutoRealm::new_from_handle(cx, window_jsobject);
220            let cx = &mut realm;
221
222            // Create a new window proxy.
223            rooted!(&in(cx) let js_proxy = handler.new_window_proxy(cx, window_jsobject));
224            assert!(!js_proxy.is_null());
225
226            // Create a new browsing context.
227
228            let current = Some(window.upcast::<GlobalScope>().pipeline_id());
229            let window_proxy = Box::new(WindowProxy::new_inherited(
230                browsing_context_id,
231                webview_id,
232                current,
233                frame_element,
234                parent,
235                opener,
236                creator,
237            ));
238
239            // The window proxy owns the browsing context.
240            // When we finalize the window proxy, it drops the browsing context it owns.
241            SetProxyReservedSlot(
242                js_proxy.get(),
243                0,
244                &PrivateValue(&raw const (*window_proxy) as *const libc::c_void),
245            );
246
247            // Notify the JS engine about the new window proxy binding.
248            SetWindowProxy(cx, window_jsobject, js_proxy.handle());
249
250            // Set the reflector.
251            debug!(
252                "Initializing reflector of {:p} to {:p}.",
253                window_proxy,
254                js_proxy.get()
255            );
256            window_proxy
257                .reflector
258                .init_reflector::<WindowProxy>(js_proxy.get());
259            DomRoot::from_ref(&*Box::into_raw(window_proxy))
260        }
261    }
262
263    #[expect(unsafe_code)]
264    pub(crate) fn new_dissimilar_origin(
265        cx: &mut JSContext,
266        global_to_clone_from: &GlobalScope,
267        browsing_context_id: BrowsingContextId,
268        webview_id: WebViewId,
269        parent: Option<&WindowProxy>,
270        opener: Option<BrowsingContextId>,
271        creator: CreatorBrowsingContextInfo,
272    ) -> DomRoot<WindowProxy> {
273        unsafe {
274            // Create a new browsing context.
275            let window_proxy = Box::new(WindowProxy::new_inherited(
276                browsing_context_id,
277                webview_id,
278                None,
279                None,
280                parent,
281                opener,
282                creator,
283            ));
284
285            // Create a new dissimilar-origin window.
286            let window = DissimilarOriginWindow::new(cx, global_to_clone_from, &window_proxy);
287            let window_jsobject = window.reflector().get_jsobject();
288            assert!(!window_jsobject.get().is_null());
289            assert_ne!(
290                ((*get_object_class(window_jsobject.get())).flags & JSCLASS_IS_GLOBAL),
291                0
292            );
293
294            let mut realm = AutoRealm::new_from_handle(cx, window_jsobject);
295            let cx = &mut realm;
296
297            // Create a new window proxy.
298            let handler = WindowProxyHandler::proxy_handler();
299            rooted!(&in(cx) let js_proxy = handler.new_window_proxy(cx, window_jsobject));
300            assert!(!js_proxy.is_null());
301
302            // The window proxy owns the browsing context.
303            // When we finalize the window proxy, it drops the browsing context it owns.
304            SetProxyReservedSlot(
305                js_proxy.get(),
306                0,
307                &PrivateValue(&raw const (*window_proxy) as *const libc::c_void),
308            );
309
310            // Notify the JS engine about the new window proxy binding.
311            SetWindowProxy(cx, window_jsobject, js_proxy.handle());
312
313            // Set the reflector.
314            debug!(
315                "Initializing reflector of {:p} to {:p}.",
316                window_proxy,
317                js_proxy.get()
318            );
319            window_proxy
320                .reflector
321                .init_reflector::<WindowProxy>(js_proxy.get());
322            DomRoot::from_ref(&*Box::into_raw(window_proxy))
323        }
324    }
325
326    /// <https://html.spec.whatwg.org/multipage/#auxiliary-browsing-context>
327    fn create_auxiliary_browsing_context(
328        &self,
329        cx: &mut JSContext,
330        name: DOMString,
331        noopener: bool,
332    ) -> Option<DomRoot<WindowProxy>> {
333        let (response_sender, response_receiver) = generic_channel::channel().unwrap();
334        let window = self
335            .currently_active
336            .get()
337            .and_then(ScriptThread::find_document)
338            .map(|doc| DomRoot::from_ref(doc.window()))
339            .unwrap();
340
341        let document = self
342            .currently_active
343            .get()
344            .and_then(ScriptThread::find_document)
345            .expect("A WindowProxy creating an auxiliary to have an active document");
346
347        // <https://html.spec.whatwg.org/multipage/#navigable-target-names>
348        // > If the user agent has been configured such that in this instance it
349        // > will create a new top-level traversable
350        // >
351        // Step 9. If sandboxingFlagSet's sandbox propagates to auxiliary browsing
352        //   contexts flag is set, then all the flags that are set in sandboxingFlagSet
353        // must be set in chosen's active browsing context's popup sandboxing flag set.
354        let sandboxing_flag_set = document.active_sandboxing_flag_set();
355        let propagate_sandbox = sandboxing_flag_set
356            .contains(SandboxingFlagSet::SANDBOX_PROPOGATES_TO_AUXILIARY_BROWSING_CONTEXTS_FLAG);
357        let sandboxing_flag_set = if propagate_sandbox {
358            sandboxing_flag_set
359        } else {
360            SandboxingFlagSet::empty()
361        };
362
363        let blank_url = ServoUrl::parse("about:blank").ok().unwrap();
364        let mut load_data = LoadData::new(
365            LoadOrigin::Script(document.origin().snapshot()),
366            blank_url,
367            Some(document.base_url()),
368            // This has the effect of ensuring that the new `about:blank` URL has the
369            // same origin as the `Document` that is creating the new browsing context.
370            Some(window.pipeline_id()),
371            document.global().get_referrer(),
372            document.get_referrer_policy(),
373            None, // Doesn't inherit secure context
374            None,
375            false,
376            sandboxing_flag_set,
377        );
378        load_data.is_initial_about_blank = true;
379        let load_info = AuxiliaryWebViewCreationRequest {
380            load_data: load_data.clone(),
381            opener_webview_id: window.webview_id(),
382            opener_pipeline_id: self.currently_active.get().unwrap(),
383            response_sender,
384        };
385        let constellation_msg = ScriptToConstellationMessage::CreateAuxiliaryWebView(load_info);
386        window.send_to_constellation(constellation_msg);
387
388        let response = response_receiver.recv().unwrap()?;
389        let new_browsing_context_id = BrowsingContextId::from(response.new_webview_id);
390        let new_pipeline_info = NewPipelineInfo {
391            webview_state: WebViewState {
392                id: response.new_webview_id,
393                // Use the current `WebView`'s theme initially, but the embedder may change
394                // this later.
395                theme: Cell::new(window.webview_theme()),
396                // WebViews start focused by default for now.
397                has_system_focus: Cell::new(true),
398            },
399            parent_info: None,
400            new_pipeline_id: response.new_pipeline_id,
401            browsing_context_id: new_browsing_context_id,
402            opener: Some(self.browsing_context_id),
403            load_data,
404            viewport_details: window.viewport_details(),
405            user_content_manager_id: response.user_content_manager_id,
406            target_snapshot_params: TargetSnapshotParams {
407                sandboxing_flags: sandboxing_flag_set,
408                iframe_element_referrer_policy: ReferrerPolicy::EmptyString,
409            },
410            frame_name: None,
411        };
412
413        with_script_thread(|script_thread| {
414            script_thread.spawn_pipeline(cx, new_pipeline_info);
415        });
416
417        let new_window_proxy = ScriptThread::find_document(response.new_pipeline_id)
418            .and_then(|doc| doc.browsing_context())?;
419        if !name.eq_ignore_ascii_case("_blank") {
420            new_window_proxy.set_name(name);
421        }
422        if noopener {
423            new_window_proxy.disown();
424        } else {
425            // After creating a new auxiliary browsing context and document,
426            // the session storage is copied over.
427            // See https://html.spec.whatwg.org/multipage/#the-sessionstorage-attribute
428
429            let (sender, receiver) = generic_channel::channel().unwrap();
430
431            let msg = WebStorageThreadMsg::Clone {
432                sender,
433                src: window.window_proxy().webview_id(),
434                dest: response.new_webview_id,
435            };
436
437            GenericSend::send(document.global().storage_threads(), msg).unwrap();
438            receiver.recv().unwrap();
439        }
440        Some(new_window_proxy)
441    }
442
443    /// <https://html.spec.whatwg.org/multipage/#delaying-load-events-mode>
444    pub(crate) fn start_delaying_load_events_mode(&self) {
445        self.delaying_load_events_mode.set(true);
446    }
447
448    /// <https://html.spec.whatwg.org/multipage/#delaying-load-events-mode>
449    pub(crate) fn stop_delaying_load_events_mode(&self) {
450        self.delaying_load_events_mode.set(false);
451    }
452
453    /// <https://html.spec.whatwg.org/multipage/#disowned-its-opener>
454    pub(crate) fn disown(&self) {
455        self.disowned.set(true);
456    }
457
458    /// <https://html.spec.whatwg.org/multipage/#dom-window-close>
459    /// Step 3.1, set BCs `is_closing` to true.
460    pub(crate) fn close(&self) {
461        self.is_closing.set(true);
462    }
463
464    /// <https://html.spec.whatwg.org/multipage/#is-closing>
465    pub(crate) fn is_closing(&self) -> bool {
466        self.is_closing.get()
467    }
468
469    /// <https://html.spec.whatwg.org/multipage/#dom-opener>
470    pub(crate) fn opener(&self, cx: &mut CurrentRealm, mut retval: MutableHandleValue) {
471        if self.disowned.get() {
472            return retval.set(NullValue());
473        }
474        let opener_id = match self.opener {
475            Some(opener_browsing_context_id) => opener_browsing_context_id,
476            None => return retval.set(NullValue()),
477        };
478        let parent_browsing_context = self.parent.as_deref();
479        let opener_proxy = match self.script_window_proxies.find_window_proxy(opener_id) {
480            Some(window_proxy) => window_proxy,
481            None => {
482                let sender_pipeline_id = self.currently_active().unwrap();
483                match ScriptThread::get_top_level_for_browsing_context(
484                    self.webview_id(),
485                    sender_pipeline_id,
486                    opener_id,
487                ) {
488                    Some(opener_top_id) => {
489                        let global_to_clone_from = GlobalScope::from_current_realm(cx);
490                        let creator =
491                            CreatorBrowsingContextInfo::from(parent_browsing_context, None);
492                        WindowProxy::new_dissimilar_origin(
493                            cx,
494                            &global_to_clone_from,
495                            opener_id,
496                            opener_top_id,
497                            None,
498                            None,
499                            creator,
500                        )
501                    },
502                    None => return retval.set(NullValue()),
503                }
504            },
505        };
506        if opener_proxy.is_browsing_context_discarded() {
507            return retval.set(NullValue());
508        }
509        opener_proxy.to_jsval(cx, retval);
510    }
511
512    /// <https://html.spec.whatwg.org/multipage/#window-open-steps>
513    pub(crate) fn open(
514        &self,
515        cx: &mut JSContext,
516        url: USVString,
517        target: DOMString,
518        features: DOMString,
519    ) -> Fallible<Option<DomRoot<WindowProxy>>> {
520        // Note: this does not map to the spec,
521        // but it does prevent a panic at the constellation because the browsing context
522        // has already been discarded.
523        // See issue: #39716 for the original problem,
524        // and https://github.com/whatwg/html/issues/11797 for a discussion at the level of the spec.
525        if self.discarded.get() {
526            return Ok(None);
527        }
528        // Step 2. Let sourceDocument be the entry global object's associated Document.
529        //
530        // It's possible to end up in a situation where JS code is executing but
531        // we have not had a chance to push an entry global (e.g. via WASM instantiation).
532        // If that happens, fall back to the active document of this browsing context.
533        let source_document = maybe_entry_global()
534            .map(|global| global.as_window().Document())
535            .or_else(|| self.document())
536            .expect("Must have an entry global or active document");
537        // Step 4. If url is not the empty string:
538        let url_record = if !url.is_empty() {
539            // Step 4.1. Set urlRecord to the result of encoding-parsing a URL given url, relative to sourceDocument.
540            let Ok(url) = source_document.encoding_parse_a_url(&url) else {
541                // Step 4.2. If urlRecord is failure, then throw a "SyntaxError" DOMException.
542                return Err(Error::Syntax(Some(format!(
543                    "Error parsing URL '{url}' relative to '{}'",
544                    source_document.url()
545                ))));
546            };
547            Some(url)
548        } else {
549            // Step 3. Let urlRecord be null.
550            None
551        };
552        // Step 5. If target is the empty string, then set target to "_blank".
553        let non_empty_target = if target.is_empty() {
554            DOMString::from_static("_blank")
555        } else {
556            target
557        };
558        // Step 6. Let tokenizedFeatures be the result of tokenizing features.
559        let tokenized_features = tokenize_open_features(features);
560        // Step 7 - 8.
561        // If tokenizedFeatures["noreferrer"] exists, then set noreferrer to
562        // the result of parsing tokenizedFeatures["noreferrer"] as a boolean feature.
563        let noreferrer = parse_open_feature_boolean(&tokenized_features, "noreferrer");
564
565        // Step 9. Let noopener be the result of getting noopener for window
566        // open with sourceDocument, tokenizedFeatures, and urlRecord.
567        let noopener = if noreferrer {
568            true
569        } else {
570            parse_open_feature_boolean(&tokenized_features, "noopener")
571        };
572        // (TODO) Step 10. Remove tokenizedFeatures["noopener"] and tokenizedFeatures["noreferrer"].
573
574        // (TODO) Step 11. Let referrerPolicy be the empty string.
575        // (TODO) Step 12. If noreferrer is true, then set referrerPolicy to "no-referrer".
576
577        // Step 13 - 14
578        // Let targetNavigable and windowType be the result of applying the rules for
579        // choosing a navigable given target, sourceDocument's node navigable, and noopener.
580        // If targetNavigable is null, then return null.
581        let (chosen, new) = match self.choose_a_navigable(cx, non_empty_target, noopener) {
582            (Some(chosen), new) => (chosen, new),
583            (None, _) => return Ok(None),
584        };
585        // TODO Step 15.2, Set up browsing context features for targetNavigable's
586        // active browsing context given tokenizedFeatures.
587        let target_document = match chosen.document() {
588            Some(target_document) => target_document,
589            None => return Ok(None),
590        };
591        let has_trustworthy_ancestor_origin = if new {
592            target_document.has_trustworthy_ancestor_or_current_origin()
593        } else {
594            false
595        };
596        let target_window = target_document.window();
597        // Step 15.3. If urlRecord is null, then set urlRecord to a URL record representing about:blank.
598        let url_record = url_record.unwrap_or(ServoUrl::parse("about:blank").unwrap());
599        // Step 15.4. If urlRecord matches about:blank, then perform the URL and history update steps given targetNavigable's active document and urlRecord.
600        //
601        // This happened in the constellation as part of creating the auxiliary browsing context.
602        if !url_record.matches_about_blank() {
603            let referrer = if noreferrer {
604                Referrer::NoReferrer
605            } else {
606                target_window.as_global_scope().get_referrer()
607            };
608            // Propagate CSP list and about-base-url from opener to new document
609            let csp_list = source_document.get_csp_list().clone();
610            target_document.set_csp_list(csp_list);
611
612            // Step 15.5 Otherwise, navigate targetNavigable to urlRecord using sourceDocument,
613            // with referrerPolicy set to referrerPolicy and exceptionsEnabled set to true.
614            // FIXME: referrerPolicy may not be used properly here. exceptionsEnabled not used.
615            let mut load_data = LoadData::new(
616                LoadOrigin::Script(source_document.origin().snapshot()),
617                url_record,
618                target_document.about_base_url(),
619                Some(target_window.pipeline_id()),
620                referrer,
621                target_document.get_referrer_policy(),
622                Some(target_window.as_global_scope().is_secure_context()),
623                Some(target_document.insecure_requests_policy()),
624                has_trustworthy_ancestor_origin,
625                target_document.creation_sandboxing_flag_set_considering_parent_iframe(),
626            );
627
628            // Handle javascript: URLs specially to report CSP violations to the source window
629            // https://html.spec.whatwg.org/multipage/#navigate-to-a-javascript:-url
630            if load_data.url.scheme() == "javascript" {
631                let existing_global = source_document.global();
632
633                // Check CSP and report violations to the source (existing) window
634                if !ScriptThread::can_navigate_to_javascript_url(
635                    cx,
636                    &existing_global,
637                    target_window.as_global_scope(),
638                    &mut load_data,
639                    None,
640                ) {
641                    // CSP blocked the navigation, don't proceed
642                    return Ok(target_document.browsing_context());
643                }
644            }
645
646            let history_handling = if new {
647                NavigationHistoryBehavior::Replace
648            } else {
649                NavigationHistoryBehavior::Push
650            };
651            navigate(cx, target_window, history_handling, false, load_data);
652        }
653        // Step 17 (Dis-owning has been done in create_auxiliary_browsing_context).
654        if noopener {
655            return Ok(None);
656        }
657        // Step 18
658        Ok(target_document.browsing_context())
659    }
660
661    /// <https://html.spec.whatwg.org/multipage/#the-rules-for-choosing-a-navigable>
662    pub(crate) fn choose_a_navigable(
663        &self,
664        cx: &mut JSContext,
665        name: DOMString,
666        noopener: bool,
667    ) -> (Option<DomRoot<WindowProxy>>, bool) {
668        // Step 1. Let chosen be null.
669        // Step 2. Let windowType be "existing or none".
670
671        // Step 3. Let sandboxingFlagSet be currentNavigable's active document's active
672        // sandboxing flag set.
673        let sandboxing_flag_set = self
674            .document()
675            .map(|document| document.active_sandboxing_flag_set())
676            .unwrap_or_default();
677
678        let chosen = if name.is_empty() || name.eq_ignore_ascii_case("_self") {
679            // Step 4. If name is the empty string or an ASCII case-insensitive match for
680            // "_self", then set chosen to currentNavigable.
681            Some((Some(DomRoot::from_ref(self)), false))
682        } else if name.eq_ignore_ascii_case("_parent") {
683            // Step 5. Otherwise, if name is an ASCII case-insensitive match for
684            // "_parent", set chosen to currentNavigable's parent, if any, and
685            // currentNavigable otherwise.
686            Some(
687                self.parent()
688                    .map(|parent| (Some(DomRoot::from_ref(parent)), false))
689                    .unwrap_or_else(|| (Some(DomRoot::from_ref(self)), false)),
690            )
691        } else if name.eq_ignore_ascii_case("_top") {
692            // Step 6. Otherwise, if name is an ASCII case-insensitive match for "_top",
693            // set chosen to currentNavigable's traversable navigable.
694            Some((Some(DomRoot::from_ref(self.top())), false))
695        } else if !name.eq_ignore_ascii_case("_blank") {
696            // Step 7. Otherwise, if name is not an ASCII case-insensitive match for
697            // "_blank" and noopener is false, then set chosen to the result of finding a
698            // navigable by target name given name and currentNavigable.
699            //
700            // Note: The noopener==false condition here seems to break WPT tests and
701            // is likely a specification bug.
702            // See <https://github.com/whatwg/html/issues/12839>
703            self.find_navigable_by_target_name(&name)
704                .map(|proxy| (Some(proxy), false))
705        } else {
706            None
707        };
708
709        if let Some(chosen) = chosen {
710            return chosen;
711        }
712
713        // Step 8. If chosen is null, then a new top-level traversable is being requested,
714        // and what happens depends on the user agent's configuration and abilities — it
715        // is determined by the rules given for the first applicable option from the
716        // following list:
717        //
718        // ↪ If currentNavigable's active window does not have transient
719        //   activation and the user agent has been configured to not show popups
720        //   (i.e., the user agent has a "popup blocker" enabled)
721        //    - The user agent may inform the user that a popup has been blocked.
722        // TODO: Implement this.
723        //
724        // ↪ If sandboxingFlagSet has the sandboxed auxiliary navigation browsing context flag set
725        //   - The user agent may report to a developer console that a popup has been blocked.
726        if sandboxing_flag_set
727            .contains(SandboxingFlagSet::SANDBOXED_AUXILIARY_NAVIGATION_BROWSING_CONTEXT_FLAG)
728        {
729            (None, false)
730        }
731        // ↪ If the user agent has been configured such that in this instance it
732        // will create a new top-level traversable
733        // TODO: Integrate `create_auxiliary_browsing_context` here and have it follow the spec.
734        else {
735            (
736                self.create_auxiliary_browsing_context(cx, name, noopener),
737                true,
738            )
739        }
740    }
741
742    /// <https://html.spec.whatwg.org/multipage/#find-a-navigable-by-target-name>
743    fn find_navigable_by_target_name(&self, name: &DOMString) -> Option<DomRoot<WindowProxy>> {
744        // Step 1. Let currentDocument be currentNavigable's active document.
745        //
746        // Step 2. Let sourceSnapshotParams be the result of snapshotting source snapshot
747        // params given currentDocument.
748        // TODO: This is unimplemented.
749        //
750        // Step 3. Let subtreesToSearch be an implementation-defined choice of one of the
751        // following:
752        //     - « currentNavigable's traversable navigable, currentNavigable »
753        //     - the inclusive ancestor navigables of currentDocument
754        //
755        // From <https://github.com/whatwg/html/issues/10848>:
756        // > WebKit and Chromium search the requesting window's subtree then search from
757        // > the top. Firefox iterates and searches from each ancestor. If there's a way to
758        // > express in the spec that the implementation-defined behavior is fixed for the
759        // > instance of the user agent, then that'd be appropriate here.
760        //
761        // We use the Webkit and Chrome approach here and the reversal is done here
762        // rather than below.
763        let top = self.top();
764        let subtrees_to_search = if top != self {
765            Either::Left([self, top])
766        } else {
767            Either::Right([self])
768        };
769
770        // Step 4. For each subtreeToSearch of subtreesToSearch, in reverse order:
771        for subtree_to_search in subtrees_to_search.into_iter() {
772            // Step 4.1. Let documentToSearch be subtreeToSearch's active document.
773            // Step 4.2. For each navigable of the inclusive descendant navigables of
774            // documentToSearch:
775            if let Some(result) =
776                subtree_to_search.find_navigable_by_target_name_in_descendants(name)
777            {
778                return Some(result);
779            }
780        }
781
782        // Step 5. Let currentTopLevelBrowsingContext be currentNavigable's active
783        // browsing context's top-level browsing context.
784        // Step 6. Let group be currentTopLevelBrowsingContext's group.
785        //
786        // TODO: Servo doesn't have a concept of the browsing context group in script, so
787        // we just look through all top-level WindowProxy instances.
788        let mut top_level_window_proxies = self.script_window_proxies.top_level_window_proxies();
789
790        // Step 7. For each topLevelBrowsingContext of group's browsing context set, in an
791        // implementation-defined order (the user agent should pick a consistent ordering,
792        // such as the most recently opened, most recently focused, or more closely
793        // related):
794        //
795        // Sorting by `BrowsingContextId` is an attempt to make the order consistent.
796        // This also ensures that newer `BrowsingContextId`s are sorted first.
797        top_level_window_proxies
798            .sort_by_key(|proxy| std::cmp::Reverse(proxy.browsing_context_id()));
799
800        for window_proxy in top_level_window_proxies {
801            // Step 7.1. If currentTopLevelBrowsingContext is topLevelBrowsingContext, then
802            // continue.
803            if &*window_proxy == top {
804                continue;
805            }
806            // Step 7.2. Let documentToSearch be topLevelBrowsingContext's active document.
807            // Step 7.3. For each navigable of the inclusive descendant navigables of
808            // documentToSearch:
809            //
810            // Step 7.3.1 If currentNavigable's active browsing context is not familiar
811            // with navigable's active browsing context, then continue.
812            //
813            // TODO: Servo does not implement the concept of "familiar with".
814            // See: <https://html.spec.whatwg.org/multipage/#familiar-with>
815            // TODO: Properly support navigables in other script threads and with
816            // dissimilar origins, which requires that they be accessible here and
817            // WindowProxy::get_name() returns something useful.
818            //
819            // Step 7.3.2. If currentNavigable is not allowed by sandboxing to navigate
820            // navigable given sourceSnapshotParams, then optionally continue.
821            // Step 7.3.3 If navigable's target name is name, then return navigable.
822            // These steps are handled by `find_navigable_by_target_name_in_descendants`.
823            if let Some(result) = window_proxy.find_navigable_by_target_name_in_descendants(name) {
824                return Some(result);
825            }
826        }
827
828        // Step 8. Return null.
829        None
830    }
831
832    /// <https://html.spec.whatwg.org/multipage/#find-a-navigable-by-target-name> step 4 and 7 substeps.
833    fn find_navigable_by_target_name_in_descendants(
834        &self,
835        name: &DOMString,
836    ) -> Option<DomRoot<WindowProxy>> {
837        // Never traverse or return a `WindowProxy` with a discarded browsing context.
838        if self.is_browsing_context_discarded() {
839            return None;
840        }
841
842        // Step 4.2.1 If currentNavigable is not allowed by sandboxing to navigate
843        // navigable given sourceSnapshotParams, then optionally continue.
844        // TODO: This is unimplemented.
845
846        // Step 4.2.2. If navigable's target name is name, then return navigable.
847        if self.get_name() == *name {
848            return Some(DomRoot::from_ref(self));
849        }
850
851        let document = self.document()?;
852        let iframes: Vec<_> = document.iframes().iter().collect();
853        iframes.iter().find_map(|iframe| {
854            iframe
855                .browsing_context_id()
856                .and_then(|browsing_context_id| {
857                    self.script_window_proxies
858                        .find_window_proxy(browsing_context_id)?
859                        .find_navigable_by_target_name_in_descendants(name)
860                })
861        })
862    }
863
864    pub(crate) fn is_auxiliary(&self) -> bool {
865        self.opener.is_some()
866    }
867
868    pub(crate) fn discard_browsing_context(&self) {
869        self.discarded.set(true);
870    }
871
872    pub(crate) fn is_browsing_context_discarded(&self) -> bool {
873        self.discarded.get()
874    }
875
876    pub(crate) fn browsing_context_id(&self) -> BrowsingContextId {
877        self.browsing_context_id
878    }
879
880    pub(crate) fn webview_id(&self) -> WebViewId {
881        self.webview_id
882    }
883
884    /// If the containing `<iframe>` of this [`WindowProxy`] is from a same-origin page,
885    /// this will return an [`Element`] of the `<iframe>` element in the realm of the parent
886    /// page.
887    ///
888    /// This returns `None` if the browsing context for this [`WindowProxy`]
889    /// has been discarded.
890    pub(crate) fn frame_element(&self) -> Option<&Element> {
891        if self.is_browsing_context_discarded() {
892            return None;
893        }
894        self.frame_element.as_deref()
895    }
896
897    pub(crate) fn document(&self) -> Option<DomRoot<Document>> {
898        self.currently_active
899            .get()
900            .and_then(ScriptThread::find_document)
901    }
902
903    pub(crate) fn parent(&self) -> Option<&WindowProxy> {
904        self.parent.as_deref()
905    }
906
907    pub(crate) fn top(&self) -> &WindowProxy {
908        let mut result = self;
909        while let Some(parent) = result.parent() {
910            result = parent;
911        }
912        result
913    }
914
915    pub(crate) fn document_origin_and_internal_ancestor_origin_objects_list(
916        &self,
917    ) -> Option<(OriginSnapshot, Vec<ImmutableOrigin>)> {
918        let pipeline_id = self.currently_active()?;
919        let (result_sender, result_receiver) = generic_channel::channel().unwrap();
920        self.global()
921            .script_to_constellation_chan()
922            .send(ScriptToConstellationMessage::GetDocumentOriginDetails(
923                pipeline_id,
924                result_sender,
925            ))
926            .ok()?;
927        result_receiver.recv().ok()?
928    }
929
930    /// <https://html.spec.whatwg.org/multipage/#internal-ancestor-origin-objects-list-creation-steps>
931    pub(crate) fn parent_origin_and_internal_ancestor_origin_objects_list(
932        &self,
933    ) -> Option<(OriginSnapshot, Vec<ImmutableOrigin>)> {
934        if let Some(frame_element) = self.frame_element() {
935            let parent_document = frame_element.owner_document();
936            // Step 4. Assert: parentDoc is fully active.
937            // TODO(47417): Once "creating a new browsing context" properly exists, remove this check
938            if !parent_document.is_fully_active() {
939                return None;
940            }
941            Some((
942                parent_document.origin().snapshot(),
943                parent_document
944                    .internal_ancestor_origin_objects_list()
945                    .clone()
946                    .expect("Must always be fully active"),
947            ))
948        } else if let Some(parent_proxy) = self.parent() {
949            // Step 4. Assert: parentDoc is fully active.
950            assert!(parent_proxy.currently_active().is_some());
951            parent_proxy.document_origin_and_internal_ancestor_origin_objects_list()
952        } else {
953            None
954        }
955    }
956
957    #[expect(unsafe_code)]
958    /// Change the Window that this WindowProxy resolves to.
959    // TODO: support setting the window proxy to a dummy value,
960    // to handle the case when the active document is in another script thread.
961    fn set_window(&self, cx: &mut JSContext, window: &GlobalScope) {
962        unsafe {
963            debug!("Setting window of {:p}.", self);
964
965            let window_jsobject = window.reflector().get_jsobject();
966            let old_js_proxy = self.reflector.get_jsobject();
967            assert!(!window_jsobject.get().is_null());
968            assert_ne!(
969                ((*get_object_class(window_jsobject.get())).flags & JSCLASS_IS_GLOBAL),
970                0
971            );
972
973            let mut realm = AutoRealm::new_from_handle(cx, window_jsobject);
974            let cx = &mut realm;
975
976            // The old window proxy no longer owns this browsing context.
977            SetProxyReservedSlot(old_js_proxy.get(), 0, &PrivateValue(ptr::null_mut()));
978            // Also drop any cached cross-origin property holders.
979            SetProxyReservedSlot(
980                old_js_proxy.get(),
981                CROSS_ORIGIN_PROPERTY_HOLDER_WEAK_MAP_SLOT,
982                &UndefinedValue(),
983            );
984
985            // Brain transplant the window proxy. Brain transplantation is
986            // usually done to move a window proxy between compartments, but
987            // that's not what we are doing here. We need to do this to retarget
988            // the proxy at a different global without updating its identity.
989            rooted!(&in(cx) let new_js_proxy = WindowProxyHandler::proxy_handler().new_window_proxy(cx, window_jsobject));
990            // Explicitly set this slot to a null pointer in case a GC occurs before we
991            // are ready to set it to a real value.
992            SetProxyReservedSlot(new_js_proxy.get(), 0, &PrivateValue(ptr::null_mut()));
993            debug!(
994                "Transplanting proxy from {:p} to {:p}.",
995                old_js_proxy.get(),
996                new_js_proxy.get()
997            );
998            rooted!(&in(cx) let new_js_proxy = JS_TransplantObject(cx, old_js_proxy, new_js_proxy.handle()));
999            debug!("Transplanted proxy is {:p}.", new_js_proxy.get());
1000
1001            // Transfer ownership of this browsing context from the old window proxy to the new one.
1002            SetProxyReservedSlot(
1003                new_js_proxy.get(),
1004                0,
1005                &PrivateValue(self as *const _ as *const libc::c_void),
1006            );
1007
1008            // Notify the JS engine about the new window proxy binding.
1009            SetWindowProxy(cx, window_jsobject, new_js_proxy.handle());
1010
1011            // Update the reflector.
1012            debug!(
1013                "Setting reflector of {:p} to {:p}.",
1014                self,
1015                new_js_proxy.get()
1016            );
1017            self.reflector.rootable().set(new_js_proxy.get());
1018        }
1019    }
1020
1021    pub(crate) fn set_pipeline_id(&self, pipeline_id: PipelineId) {
1022        self.currently_active.set(Some(pipeline_id));
1023    }
1024
1025    pub(crate) fn set_currently_active(&self, cx: &mut JSContext, window: &Window) {
1026        if let Some(pipeline_id) = self.currently_active() &&
1027            pipeline_id == window.pipeline_id()
1028        {
1029            return debug!(
1030                "Attempt to set the currently active window to the currently active window."
1031            );
1032        }
1033
1034        let global_scope = window.as_global_scope();
1035        self.set_window(cx, global_scope);
1036        self.currently_active.set(Some(global_scope.pipeline_id()));
1037    }
1038
1039    pub(crate) fn unset_currently_active(&self, cx: &mut JSContext) {
1040        if self.currently_active().is_none() {
1041            return debug!(
1042                "Attempt to unset the currently active window on a windowproxy that does not have one."
1043            );
1044        }
1045        let globalscope = self.global();
1046        let window = DissimilarOriginWindow::new(cx, &globalscope, self);
1047        self.set_window(cx, window.upcast());
1048        self.currently_active.set(None);
1049    }
1050
1051    pub(crate) fn currently_active(&self) -> Option<PipelineId> {
1052        self.currently_active.get()
1053    }
1054
1055    pub(crate) fn get_name(&self) -> DOMString {
1056        self.name.borrow().clone().root()
1057    }
1058
1059    pub(crate) fn set_name(&self, name: DOMString) {
1060        *self.name.borrow_mut() = name.traced();
1061    }
1062}
1063
1064/// A browsing context can have a creator browsing context, the browsing context that
1065/// was responsible for its creation. If a browsing context has a parent browsing context,
1066/// then that is its creator browsing context. Otherwise, if the browsing context has an
1067/// opener browsing context, then that is its creator browsing context. Otherwise, the
1068/// browsing context has no creator browsing context.
1069///
1070/// If a browsing context A has a creator browsing context, then the Document that was the
1071/// active document of that creator browsing context at the time A was created is the creator
1072/// Document.
1073///
1074/// See: <https://html.spec.whatwg.org/multipage/#creating-browsing-contexts>
1075#[derive(Debug, Deserialize, Serialize)]
1076pub(crate) struct CreatorBrowsingContextInfo {
1077    /// Creator document URL.
1078    url: Option<ServoUrl>,
1079
1080    /// Creator document origin.
1081    origin: Option<ImmutableOrigin>,
1082}
1083
1084impl CreatorBrowsingContextInfo {
1085    pub(crate) fn from(
1086        parent: Option<&WindowProxy>,
1087        opener: Option<&WindowProxy>,
1088    ) -> CreatorBrowsingContextInfo {
1089        let creator = match (parent, opener) {
1090            (Some(parent), _) => parent.document(),
1091            (None, Some(opener)) => opener.document(),
1092            (None, None) => None,
1093        };
1094
1095        let url = creator.as_deref().map(|document| document.url());
1096        let origin = creator
1097            .as_deref()
1098            .map(|document| document.origin().immutable().clone());
1099
1100        CreatorBrowsingContextInfo { url, origin }
1101    }
1102}
1103
1104/// <https://html.spec.whatwg.org/multipage/#concept-window-open-features-tokenize>
1105fn tokenize_open_features(features: DOMString) -> IndexMap<String, String> {
1106    let is_feature_sep = |c: char| c.is_ascii_whitespace() || ['=', ','].contains(&c);
1107    // Step 1
1108    let mut tokenized_features = IndexMap::new();
1109    // Step 2
1110    let features = features.str();
1111    let mut iter = features.chars();
1112    let mut cur = iter.next();
1113
1114    // Step 3
1115    while cur.is_some() {
1116        // Step 3.1 & 3.2
1117        let mut name = String::new();
1118        let mut value = String::new();
1119        // Step 3.3
1120        while let Some(cur_char) = cur {
1121            if !is_feature_sep(cur_char) {
1122                break;
1123            }
1124            cur = iter.next();
1125        }
1126        // Step 3.4
1127        while let Some(cur_char) = cur {
1128            if is_feature_sep(cur_char) {
1129                break;
1130            }
1131            name.push(cur_char.to_ascii_lowercase());
1132            cur = iter.next();
1133        }
1134        // Step 3.5
1135        let normalized_name = String::from(match name.as_ref() {
1136            "screenx" => "left",
1137            "screeny" => "top",
1138            "innerwidth" => "width",
1139            "innerheight" => "height",
1140            _ => name.as_ref(),
1141        });
1142        // Step 3.6
1143        while let Some(cur_char) = cur {
1144            if cur_char == '=' || cur_char == ',' || !is_feature_sep(cur_char) {
1145                break;
1146            }
1147            cur = iter.next();
1148        }
1149        // Step 3.7
1150        if cur.is_some() && is_feature_sep(cur.unwrap()) {
1151            // Step 3.7.1
1152            while let Some(cur_char) = cur {
1153                if !is_feature_sep(cur_char) || cur_char == ',' {
1154                    break;
1155                }
1156                cur = iter.next();
1157            }
1158            // Step 3.7.2
1159            while let Some(cur_char) = cur {
1160                if is_feature_sep(cur_char) {
1161                    break;
1162                }
1163                value.push(cur_char.to_ascii_lowercase());
1164                cur = iter.next();
1165            }
1166        }
1167        // Step 3.8
1168        if !name.is_empty() {
1169            tokenized_features.insert(normalized_name, value);
1170        }
1171    }
1172    // Step 4
1173    tokenized_features
1174}
1175
1176/// <https://html.spec.whatwg.org/multipage/#concept-window-open-features-parse-boolean>
1177fn parse_open_feature_boolean(tokenized_features: &IndexMap<String, String>, name: &str) -> bool {
1178    if let Some(value) = tokenized_features.get(name) {
1179        // Step 1 & 2
1180        if value.is_empty() || value == "yes" {
1181            return true;
1182        }
1183        // Step 3 & 4
1184        if let Ok(int) = parse_integer(value.chars()) {
1185            return int != 0;
1186        }
1187    }
1188    // Step 5
1189    false
1190}
1191
1192#[expect(unsafe_code)]
1193fn window_proxy_target(proxy: HandleObject) -> *mut JSObject {
1194    let mut slot = UndefinedValue();
1195    unsafe { GetProxyPrivate(proxy.as_raw(), &mut slot) };
1196    slot.to_object()
1197}
1198
1199/// <https://html.spec.whatwg.org/multipage/#windowproxy-getownproperty>
1200#[expect(unsafe_code)]
1201unsafe extern "C" fn get_own_property_descriptor(
1202    cx: *mut RawJSContext,
1203    proxy: RawHandleObject,
1204    id: RawHandleId,
1205    property_descriptor: RawMutableHandle<PropertyDescriptor>,
1206    is_none: *mut bool,
1207) -> bool {
1208    let mut cx = unsafe {
1209        // SAFETY: We are in a SpiderMonkey hook, so it is always safe to convert a raw context into
1210        // a mozjs context.
1211        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1212    };
1213    let mut cx = CurrentRealm::assert(&mut cx);
1214    let cx = &mut cx;
1215    let proxy = unsafe { Handle::from_raw(proxy) };
1216    let id = unsafe { Handle::from_raw(id) };
1217    let mut property_descriptor = unsafe { MutableHandle::from_raw(property_descriptor) };
1218    let is_none = unsafe { &mut *is_none };
1219
1220    // Step 1. Let W be the value of the [[Window]] internal slot of this.
1221    rooted!(&in(cx) let target = window_proxy_target(proxy));
1222    let window = WindowOrDissimilarOriginWindow::new(cx, target.handle());
1223
1224    // Step 2. If P is an array index property name:
1225    // Step 2.1. Let index be ! ToUint32(P).
1226    if let Some(index) = get_array_index_from_id(id) {
1227        // Step 2.2. Let children be the document-tree child navigables of W's associated Document.
1228        // Step 2.3. Let value be undefined.
1229        // Step 2.4. If index is less than children's size:
1230        if let Some(window_proxy) = window.window_proxy_for_child_navigable_at_index(index) {
1231            // Step 2.4.1. Sort children in ascending order, with navigableA being less than
1232            // navigableB if navigableA's container was inserted into W's
1233            // associated Document earlier than navigableB's container was.
1234            // Step 2.4.2. Set value to children[index]'s active WindowProxy.
1235            //
1236            // Note: These are handled by `window_proxy_for_child_navigable_at_index`.
1237            rooted!(&in(cx) let mut window_proxy_jsval = UndefinedValue());
1238            window_proxy.to_jsval(cx, window_proxy_jsval.handle_mut());
1239
1240            // 2.6. Return PropertyDescriptor { [[Value]]: value, [[Writable]]:
1241            // false, [[Enumerable]]: true, [[Configurable]]: true }.
1242            set_property_descriptor(
1243                property_descriptor,
1244                window_proxy_jsval.handle(),
1245                (JSPROP_ENUMERATE | JSPROP_READONLY) as u32,
1246                is_none,
1247            );
1248            return true;
1249        } else {
1250            // Step 2.5. If value is undefined:
1251            *is_none = true;
1252            // Step 2.5.1 If IsPlatformObjectSameOrigin(W) is true, then return
1253            // undefined.
1254            if is_platform_object_same_origin(cx, proxy) {
1255                return true;
1256            }
1257            // Step 2.5.2 Throw a "SecurityError" DOMException.
1258            return report_cross_origin_denial::<DomTypeHolder>(cx, id, "get");
1259        }
1260    }
1261
1262    // Step 3. If IsPlatformObjectSameOrigin(W) is true, then return ! OrdinaryGetOwnProperty(W, P).
1263    if is_platform_object_same_origin(cx, proxy) {
1264        return JS_GetOwnPropertyDescriptorById(
1265            cx,
1266            target.handle(),
1267            id,
1268            property_descriptor,
1269            is_none,
1270        );
1271    }
1272
1273    // Step 4. Let property be CrossOriginGetOwnPropertyHelper(W, P).
1274    if !cross_origin_get_own_property_helper(
1275        cx,
1276        proxy,
1277        window.cross_origin_properties(),
1278        id,
1279        property_descriptor.reborrow(),
1280        is_none,
1281    ) {
1282        return false;
1283    }
1284
1285    // Step 5. If property is not undefined, then return property.
1286    if !*is_none {
1287        return true;
1288    }
1289
1290    // Step 6. If property is undefined and P is in W's document-tree child navigable target name
1291    // property set:
1292    if let Some(named_child_navigable) = window.named_child_navigable(cx, id) {
1293        // Step 6.1. Let value be the active WindowProxy of the named object of W with the name P.
1294        rooted!(&in(cx) let mut window_proxy_value = UndefinedValue());
1295        named_child_navigable.to_jsval(cx, window_proxy_value.handle_mut());
1296        // Step 6.2 Return PropertyDescriptor { [[Value]]: value, [[Enumerable]]: false,
1297        // [[Writable]]: false, [[Configurable]]: true }.
1298        set_property_descriptor(
1299            property_descriptor.reborrow(),
1300            window_proxy_value.handle(),
1301            JSPROP_READONLY as u32,
1302            is_none,
1303        );
1304        return true;
1305    }
1306
1307    // Step 7. Return ? CrossOriginPropertyFallback(P).
1308    cross_origin_property_fallback::<DomTypeHolder>(
1309        cx,
1310        proxy,
1311        id,
1312        property_descriptor.reborrow(),
1313        is_none,
1314    )
1315}
1316
1317/// <https://html.spec.whatwg.org/multipage/#windowproxy-defineownproperty>
1318#[expect(unsafe_code)]
1319unsafe extern "C" fn define_property(
1320    cx: *mut RawJSContext,
1321    proxy: RawHandleObject,
1322    id: RawHandleId,
1323    desc: RawHandle<PropertyDescriptor>,
1324    res: *mut ObjectOpResult,
1325) -> bool {
1326    let mut cx = unsafe {
1327        // SAFETY: We are in a SpiderMonkey hook, so it is always safe to convert a raw context into
1328        // a mozjs context.
1329        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1330    };
1331    let mut cx = CurrentRealm::assert(&mut cx);
1332    let cx = &mut cx;
1333    let id = unsafe { Handle::from_raw(id) };
1334    let proxy = unsafe { Handle::from_raw(proxy) };
1335
1336    // Step 1. Let W be the value of the [[Window]] internal slot of this.
1337    // Note: This is the `proxy` argument.
1338
1339    // Step 2. If IsPlatformObjectSameOrigin(W) is true:
1340    if is_platform_object_same_origin(cx, proxy) {
1341        // Step 2.1. If P is an array index property name, return false.
1342        if get_array_index_from_id(id).is_some() {
1343            // Spec says to Reject whether this is a supported index or not,
1344            // since we have no indexed setter or indexed creator.  That means
1345            // throwing in strict mode (FIXME: Bug 828137), doing nothing in
1346            // non-strict mode.
1347            unsafe {
1348                (*res).code_ = JSErrNum::JSMSG_CANT_DEFINE_WINDOW_ELEMENT as usize;
1349            }
1350            return true;
1351        }
1352
1353        // Step 2.2. Return ? OrdinaryDefineOwnProperty(W, P, Desc).
1354        rooted!(&in(cx) let target = window_proxy_target(proxy));
1355        return unsafe {
1356            JS_DefinePropertyById(cx.raw_cx(), target.handle().into(), id.into(), desc, res)
1357        };
1358    }
1359
1360    // Step 3. Throw a "SecurityError" DOMException.
1361    report_cross_origin_denial::<DomTypeHolder>(cx, id, "define")
1362}
1363
1364#[expect(unsafe_code)]
1365unsafe extern "C" fn has(
1366    cx: *mut RawJSContext,
1367    proxy: RawHandleObject,
1368    id: RawHandleId,
1369    bp: *mut bool,
1370) -> bool {
1371    unsafe { has_or_has_own(cx, proxy, id, bp, IncludePrototypes::Yes) }
1372}
1373
1374#[expect(unsafe_code)]
1375unsafe extern "C" fn has_own(
1376    cx: *mut RawJSContext,
1377    proxy: RawHandleObject,
1378    id: RawHandleId,
1379    bp: *mut bool,
1380) -> bool {
1381    unsafe { has_or_has_own(cx, proxy, id, bp, IncludePrototypes::No) }
1382}
1383
1384enum IncludePrototypes {
1385    Yes,
1386    No,
1387}
1388
1389#[expect(unsafe_code)]
1390unsafe fn has_or_has_own(
1391    cx: *mut RawJSContext,
1392    proxy: RawHandleObject,
1393    id: RawHandleId,
1394    bp: *mut bool,
1395    include_prototypes: IncludePrototypes,
1396) -> bool {
1397    let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
1398    let mut cx = CurrentRealm::assert(&mut cx);
1399    let cx = &mut cx;
1400    let proxy = unsafe { Handle::from_raw(proxy) };
1401    let id = unsafe { Handle::from_raw(id) };
1402
1403    rooted!(&in(cx) let target = window_proxy_target(proxy));
1404    let window = WindowOrDissimilarOriginWindow::new(cx, target.handle());
1405
1406    let (success, found) = if is_platform_object_same_origin(cx, proxy) {
1407        if let Some(array_index) = get_array_index_from_id(id) &&
1408            window
1409                .window_proxy_for_child_navigable_at_index(array_index)
1410                .is_some()
1411        {
1412            unsafe { *bp = true };
1413            return true;
1414        }
1415
1416        let mut found = false;
1417        let success = match include_prototypes {
1418            IncludePrototypes::Yes => unsafe {
1419                JS_HasPropertyById(cx, target.handle(), id, &mut found)
1420            },
1421            IncludePrototypes::No => unsafe {
1422                JS_HasOwnPropertyById(cx, target.handle(), id, &mut found)
1423            },
1424        };
1425        (success, found)
1426    } else {
1427        rooted!(&in(cx) let mut property_descriptor = PropertyDescriptor::default());
1428        let mut is_none = false;
1429        let success = unsafe {
1430            get_own_property_descriptor(
1431                cx.raw_cx(),
1432                proxy.into(),
1433                id.into(),
1434                property_descriptor.handle_mut().into(),
1435                &mut is_none,
1436            )
1437        };
1438        (success, !is_none)
1439    };
1440
1441    if !success {
1442        return false;
1443    }
1444    unsafe { *bp = found };
1445    true
1446}
1447
1448/// <https://html.spec.whatwg.org/multipage/#windowproxy-get>
1449#[expect(unsafe_code)]
1450unsafe extern "C" fn get(
1451    cx: *mut RawJSContext,
1452    proxy: RawHandleObject,
1453    receiver: RawHandleValue,
1454    id: RawHandleId,
1455    return_value: RawMutableHandleValue,
1456) -> bool {
1457    let mut cx = unsafe {
1458        // SAFETY: We are in SM hook
1459        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1460    };
1461    let mut cx = CurrentRealm::assert(&mut cx);
1462    let cx = &mut cx;
1463    let proxy = unsafe { Handle::from_raw(proxy) };
1464    let receiver = unsafe { Handle::from_raw(receiver) };
1465    let id = unsafe { Handle::from_raw(id) };
1466    let return_value = unsafe { MutableHandle::from_raw(return_value) };
1467
1468    // Step 1. Let W be the value of the [[Window]] internal slot of this.
1469    rooted!(&in(cx) let target = window_proxy_target(proxy));
1470    let window = WindowOrDissimilarOriginWindow::new(cx, target.handle());
1471
1472    // Step 2. Check if an access between two browsing contexts should be reported, given the
1473    // current global object's browsing context, W's browsing context, P, and the current settings
1474    // object.
1475    // TODO: Implement this.
1476
1477    // Step 3. If IsPlatformObjectSameOrigin(W) is true, then return ? OrdinaryGet(this, P, Receiver).
1478    if is_platform_object_same_origin(cx, proxy) {
1479        if let Some(index) = get_array_index_from_id(id) &&
1480            let Some(window_proxy) = window.window_proxy_for_child_navigable_at_index(index)
1481        {
1482            window_proxy.to_jsval(cx, return_value);
1483            return true;
1484        }
1485
1486        return unsafe { JS_ForwardGetPropertyTo(cx, target.handle(), id, receiver, return_value) };
1487    }
1488
1489    // Step 4. Return ? CrossOriginGet(this, P, Receiver).
1490    proxyhandler::cross_origin_get::<DomTypeHolder>(cx, proxy, receiver, id, return_value)
1491}
1492
1493/// <https://html.spec.whatwg.org/multipage/#windowproxy-set>
1494#[expect(unsafe_code)]
1495unsafe extern "C" fn set(
1496    cx: *mut RawJSContext,
1497    proxy: RawHandleObject,
1498    id: RawHandleId,
1499    v: RawHandleValue,
1500    receiver: RawHandleValue,
1501    res: *mut ObjectOpResult,
1502) -> bool {
1503    let mut cx = unsafe {
1504        // SAFETY: We are in SM hook
1505        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1506    };
1507    let mut cx = CurrentRealm::assert(&mut cx);
1508    let cx = &mut cx;
1509
1510    // Step 1. Let W be the value of the [[Window]] internal slot of this.
1511    // Note: This is the `proxy` argument.
1512
1513    // Step 2. Check if an access between two browsing contexts should be reported, given the
1514    // current global object's browsing context, W's browsing context, P, and the current settings
1515    // object.
1516    // TODO: Implement this.
1517
1518    let proxy = unsafe { Handle::from_raw(proxy) };
1519    let id = unsafe { Handle::from_raw(id) };
1520
1521    // Step 3. If IsPlatformObjectSameOrigin(W) is true:
1522    if is_platform_object_same_origin(cx, proxy) {
1523        // Step 3.1. If P is an array index property name, then return false.
1524        if get_array_index_from_id(id).is_some() {
1525            // Reject (which means throw if and only if strict) the set.
1526            unsafe { (*res).code_ = JSErrNum::JSMSG_READ_ONLY as usize };
1527            return true;
1528        }
1529
1530        // Step 3.2. Return ? OrdinarySet(W, P, V, Receiver).
1531        rooted!(&in(cx) let target = window_proxy_target(proxy));
1532        return unsafe {
1533            JS_ForwardSetPropertyTo(
1534                cx.raw_cx(),
1535                target.handle().into(),
1536                id.into(),
1537                v,
1538                receiver,
1539                res,
1540            )
1541        };
1542    }
1543
1544    // Step 4. Return ? CrossOriginSet(this, P, V, Receiver).
1545    let receiver = unsafe { HandleValue::from_raw(receiver) };
1546    unsafe { cross_origin_set::<DomTypeHolder>(cx, proxy, id, v, receiver, res) }
1547}
1548
1549#[expect(unsafe_code)]
1550unsafe extern "C" fn get_prototype_if_ordinary(
1551    _: *mut RawJSContext,
1552    _: RawHandleObject,
1553    is_ordinary: *mut bool,
1554    _: RawMutableHandleObject,
1555) -> bool {
1556    // Window's [[GetPrototypeOf]] trap isn't the ordinary definition:
1557    //
1558    //   https://html.spec.whatwg.org/multipage/#windowproxy-getprototypeof
1559    //
1560    // We nonetheless can implement it with a static [[Prototype]], because
1561    // wrapper-class handlers (particularly, XOW in FilteringWrapper.cpp) supply
1562    // all non-ordinary behavior.
1563    //
1564    // But from a spec point of view, it's the exact same object in both cases --
1565    // only the observer's changed.  So this getPrototypeIfOrdinary trap on the
1566    // non-wrapper object *must* report non-ordinary, even if static [[Prototype]]
1567    // usually means ordinary.
1568    unsafe { *is_ordinary = false };
1569    true
1570}
1571
1572/// <https://html.spec.whatwg.org/multipage/#windowproxy-getprototypeof>
1573#[expect(unsafe_code)]
1574unsafe extern "C" fn get_prototype(
1575    cx: *mut RawJSContext,
1576    proxy: RawHandleObject,
1577    result: RawMutableHandleObject,
1578) -> bool {
1579    let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
1580    let mut realm = CurrentRealm::assert(&mut cx);
1581    let proxy = unsafe { Handle::from_raw(proxy) };
1582    let result = unsafe { MutableHandleObject::from_raw(result) };
1583    maybe_cross_origin_get_prototype::<DomTypeHolder>(
1584        &mut realm,
1585        proxy,
1586        GetProtoObject::<DomTypeHolder>,
1587        result,
1588    )
1589}
1590
1591/// <https://html.spec.whatwg.org/multipage/#windowproxy-delete>
1592#[expect(unsafe_code)]
1593unsafe extern "C" fn delete(
1594    cx: *mut RawJSContext,
1595    proxy: RawHandleObject,
1596    id: RawHandleId,
1597    result: *mut ObjectOpResult,
1598) -> bool {
1599    let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
1600    let mut cx = CurrentRealm::assert(&mut cx);
1601    let cx = &mut cx;
1602    let proxy = unsafe { Handle::from_raw(proxy) };
1603    let id = unsafe { Handle::from_raw(id) };
1604
1605    // Step 1. Let W be the value of the [[Window]] internal slot of this.
1606    rooted!(&in(cx) let target = window_proxy_target(proxy));
1607    let window = WindowOrDissimilarOriginWindow::new(cx, target.handle());
1608
1609    // Step 2. If IsPlatformObjectSameOrigin(W) is true:
1610    if is_platform_object_same_origin(cx, proxy) {
1611        // Step 2.1 If P is an array index property name:
1612        if let Some(array_index) = get_array_index_from_id(id) {
1613            // Step 2.1.1. Let desc be ! this.[[GetOwnProperty]](P).
1614            let code = if window
1615                .window_proxy_for_child_navigable_at_index(array_index)
1616                .is_none()
1617            {
1618                // Step 2.1.2. If desc is undefined, then return true.
1619                0 /* OkCode */
1620            } else {
1621                // Step 2.1.3. Return false.
1622                JSErrNum::JSMSG_CANT_DELETE_WINDOW_ELEMENT as usize
1623            };
1624            unsafe { (*result).code_ = code };
1625            return true;
1626        }
1627        // Step 2.2. Return ? OrdinaryDelete(W, P).
1628        rooted!(&in(cx) let target = window_proxy_target(proxy));
1629        return unsafe {
1630            JS_DeletePropertyById(cx.raw_cx(), target.handle().into(), id.into(), result)
1631        };
1632    }
1633
1634    // Step 3. Throw a "SecurityError" DOMException.
1635    report_cross_origin_denial::<DomTypeHolder>(cx, id, "delete")
1636}
1637
1638/// <https://html.spec.whatwg.org/multipage#windowproxy-ownpropertykeys>
1639#[expect(unsafe_code)]
1640unsafe extern "C" fn own_property_keys(
1641    cx: *mut RawJSContext,
1642    proxy: RawHandleObject,
1643    property_keys: RawMutableHandleIdVector,
1644) -> bool {
1645    let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
1646    let mut cx = CurrentRealm::assert(&mut cx);
1647    let cx = &mut cx;
1648    let proxy = unsafe { Handle::from_raw(proxy) };
1649
1650    // Step 1. Let W be the value of the [[Window]] internal slot of this.
1651    rooted!(&in(cx) let target = window_proxy_target(proxy));
1652    let window = WindowOrDissimilarOriginWindow::new(cx, target.handle());
1653
1654    // Step 2. Let maxProperties be W's associated Document's document-tree child navigables's size.
1655    let max_properties = window.iframe_count();
1656
1657    // Step 3. Let keys be the range 0 to maxProperties, exclusive.
1658    rooted!(&in(cx) let mut rooted_index_jsid: jsid);
1659    for index in 0..max_properties {
1660        unsafe { int_to_jsid(index as i32, rooted_index_jsid.handle_mut()) };
1661        unsafe { AppendToIdVector(property_keys, rooted_index_jsid.handle()) };
1662    }
1663
1664    if is_platform_object_same_origin(cx, proxy) {
1665        // Step 4. If IsPlatformObjectSameOrigin(W) is true, then return the concatenation of keys and
1666        // OrdinaryOwnPropertyKeys(W).
1667        return GetPropertyKeys(
1668            cx,
1669            target.handle(),
1670            JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS,
1671            property_keys,
1672        );
1673    }
1674
1675    // Step 5. Return the concatenation of keys and ! CrossOriginOwnPropertyKeys(W).
1676    cross_origin_own_property_keys(cx, proxy, window.cross_origin_properties(), property_keys)
1677}
1678
1679/// A version of <https://html.spec.whatwg.org/multipage#windowproxy-ownpropertykeys>
1680/// that hands back only the enumerable properties. This is necessary because the
1681/// default implementation of this method returns all enumerable properties which
1682/// isn't correct in the cross-origin case.
1683#[expect(unsafe_code)]
1684unsafe extern "C" fn get_own_enumerable_property_keys(
1685    cx: *mut RawJSContext,
1686    proxy: RawHandleObject,
1687    property_keys: RawMutableHandleIdVector,
1688) -> bool {
1689    let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
1690    let mut cx = CurrentRealm::assert(&mut cx);
1691    let cx = &mut cx;
1692    let proxy = unsafe { Handle::from_raw(proxy) };
1693
1694    // Step 1. Let W be the value of the [[Window]] internal slot of this.
1695    rooted!(&in(cx) let target = window_proxy_target(proxy));
1696    let window = WindowOrDissimilarOriginWindow::new(cx, target.handle());
1697
1698    // Step 2. Let maxProperties be W's associated Document's document-tree child navigables's size.
1699    let max_properties = window.iframe_count();
1700
1701    // Step 3. Let keys be the range 0 to maxProperties, exclusive.
1702    rooted!(&in(cx) let mut rooted_index_jsid: jsid);
1703    for index in 0..max_properties {
1704        unsafe { int_to_jsid(index as i32, rooted_index_jsid.handle_mut()) };
1705        unsafe { AppendToIdVector(property_keys, rooted_index_jsid.handle()) };
1706    }
1707
1708    if is_platform_object_same_origin(cx, proxy) {
1709        // Step 4. If IsPlatformObjectSameOrigin(W) is true, then return the concatenation of keys and
1710        // OrdinaryOwnPropertyKeys(W).
1711        return GetPropertyKeys(cx, target.handle(), JSITER_OWNONLY, property_keys);
1712    }
1713
1714    // There are no other enumerable property keys for cross-origin WindowProxy other than
1715    // the child navigable indices.
1716    true
1717}
1718
1719#[expect(unsafe_code)]
1720unsafe extern "C" fn enumerate(
1721    cx: *mut RawJSContext,
1722    proxy: RawHandleObject,
1723    property_keys: RawMutableHandleIdVector,
1724) -> bool {
1725    // Just get the property keys from ourselves, in whatever Realm we happen to
1726    // be in. It's important to not enter the Realm of "proxy" here, because that
1727    // would affect the list of keys we claim to have.
1728    let mut cx = unsafe { JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
1729    let cx = &mut cx;
1730    let proxy = unsafe { Handle::from_raw(proxy) };
1731
1732    GetPropertyKeys(cx, proxy, 0, property_keys)
1733}
1734
1735static PROXY_TRAPS: ProxyTraps = ProxyTraps {
1736    enter: None,
1737    getOwnPropertyDescriptor: Some(get_own_property_descriptor),
1738    defineProperty: Some(define_property),
1739    ownPropertyKeys: Some(own_property_keys),
1740    delete_: Some(delete),
1741    enumerate: Some(enumerate),
1742    getPrototypeIfOrdinary: Some(get_prototype_if_ordinary),
1743    getPrototype: Some(get_prototype),
1744    setPrototype: Some(maybe_cross_origin_set_prototype_rawcx),
1745    setImmutablePrototype: None,
1746    preventExtensions: Some(prevent_extensions),
1747    isExtensible: Some(is_extensible),
1748    has: Some(has),
1749    get: Some(get),
1750    set: Some(set),
1751    call: None,
1752    construct: None,
1753    hasOwn: Some(has_own),
1754    getOwnEnumerablePropertyKeys: Some(get_own_enumerable_property_keys),
1755    nativeCall: None,
1756    objectClassIs: None,
1757    className: None,
1758    fun_toString: None,
1759    boxedValue_unbox: None,
1760    defaultValue: None,
1761    trace: Some(trace),
1762    finalize: Some(finalize),
1763    objectMoved: None,
1764    isCallable: None,
1765    isConstructor: None,
1766};
1767
1768/// Proxy handler for a WindowProxy.
1769/// Has ownership of the inner pointer and deallocates it when it is no longer needed.
1770pub(crate) struct WindowProxyHandler(*const libc::c_void);
1771
1772impl MallocSizeOf for WindowProxyHandler {
1773    fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
1774        // FIXME(#6907) this is a pointer to memory allocated by `new` in NewProxyHandler in rust-mozjs.
1775        0
1776    }
1777}
1778
1779// Safety: Send and Sync is guaranteed since the underlying pointer and all its associated methods in C++ are const.
1780#[expect(unsafe_code)]
1781unsafe impl Send for WindowProxyHandler {}
1782// Safety: Send and Sync is guaranteed since the underlying pointer and all its associated methods in C++ are const.
1783#[expect(unsafe_code)]
1784unsafe impl Sync for WindowProxyHandler {}
1785
1786#[expect(unsafe_code)]
1787impl WindowProxyHandler {
1788    fn new(traps: &ProxyTraps) -> Self {
1789        // Safety: Foreign function generated by bindgen. Pointer is freed in drop to prevent memory leak.
1790        let ptr = unsafe { CreateWrapperProxyHandler(traps) };
1791        assert!(!ptr.is_null());
1792        Self(ptr)
1793    }
1794
1795    /// Returns a single, shared WindowProxyHandler that contains normal PROXY_TRAPS.
1796    pub(crate) fn proxy_handler() -> &'static Self {
1797        use std::sync::OnceLock;
1798        /// We are sharing a single instance for the entire programs here due to lifetime issues.
1799        /// The pointer in self.0 is known to C++ and visited by the GC. Hence, we don't know when
1800        /// it is safe to free it.
1801        /// Sharing a single instance should be fine because all methods on this pointer in C++
1802        /// are const and don't modify its internal state.
1803        static SINGLETON: OnceLock<WindowProxyHandler> = OnceLock::new();
1804        SINGLETON.get_or_init(|| Self::new(&PROXY_TRAPS))
1805    }
1806
1807    /// Creates a new WindowProxy object on the C++ side and returns the pointer to it.
1808    /// The pointer should be owned by the GC.
1809    fn new_window_proxy(
1810        &self,
1811        cx: &mut JSContext,
1812        window_jsobject: js::gc::HandleObject,
1813    ) -> *mut JSObject {
1814        let obj = unsafe { NewWindowProxy(cx, window_jsobject, self.0) };
1815        assert!(!obj.is_null());
1816        obj
1817    }
1818}
1819
1820#[expect(unsafe_code)]
1821impl Drop for WindowProxyHandler {
1822    fn drop(&mut self) {
1823        // Safety: Pointer is allocated by corresponding C++ function, owned by this
1824        // struct and not accessible from outside.
1825        unsafe {
1826            DeleteWrapperProxyHandler(self.0);
1827        }
1828    }
1829}
1830
1831// How WindowProxy objects are garbage collected.
1832
1833#[expect(unsafe_code)]
1834unsafe extern "C" fn finalize(_fop: *mut GCContext, obj: *mut JSObject) {
1835    let mut slot = UndefinedValue();
1836    unsafe { GetProxyReservedSlot(obj, 0, &mut slot) };
1837    let this = slot.to_private() as *mut WindowProxy;
1838    if this.is_null() {
1839        // GC during obj creation or after transplanting.
1840        return;
1841    }
1842    unsafe {
1843        (*this).reflector.drop_memory(&*this);
1844        let jsobject = (*this).reflector.get_jsobject().get();
1845        debug!(
1846            "WindowProxy finalize: {:p}, with reflector {:p} from {:p}.",
1847            this, jsobject, obj
1848        );
1849        let _ = Box::from_raw(this);
1850    }
1851}
1852
1853#[expect(unsafe_code)]
1854unsafe extern "C" fn trace(trc: *mut JSTracer, obj: *mut JSObject) {
1855    let mut slot = UndefinedValue();
1856    unsafe { GetProxyReservedSlot(obj, 0, &mut slot) };
1857    let this = slot.to_private() as *const WindowProxy;
1858    if this.is_null() {
1859        // GC during obj creation or after transplanting.
1860        return;
1861    }
1862    unsafe { (*this).trace(trc) };
1863}
1864
1865/// A wrapper class for either a [`Window`] or [`DissimilarOriginWindow`] that
1866/// exposes a consistent interface for both of them.
1867enum WindowOrDissimilarOriginWindow {
1868    Window(DomRoot<Window>),
1869    DissimilarOriginWindow(DomRoot<DissimilarOriginWindow>),
1870}
1871
1872impl WindowOrDissimilarOriginWindow {
1873    fn new(cx: &mut JSContext, handle_object: HandleObject) -> Self {
1874        if let Ok(window) = root_from_handleobject::<Window>(cx, handle_object) {
1875            Self::Window(window)
1876        } else if let Ok(window) =
1877            root_from_handleobject::<DissimilarOriginWindow>(cx, handle_object)
1878        {
1879            Self::DissimilarOriginWindow(window)
1880        } else {
1881            unreachable!("WindowProxy should always be backed by some kind of window");
1882        }
1883    }
1884
1885    fn global_scope(&self) -> DomRoot<GlobalScope> {
1886        match self {
1887            WindowOrDissimilarOriginWindow::Window(window) => window.global(),
1888            WindowOrDissimilarOriginWindow::DissimilarOriginWindow(window) => window.global(),
1889        }
1890    }
1891
1892    fn window_proxy(&self) -> DomRoot<WindowProxy> {
1893        match self {
1894            WindowOrDissimilarOriginWindow::Window(window) => window.window_proxy(),
1895            WindowOrDissimilarOriginWindow::DissimilarOriginWindow(window) => window.window_proxy(),
1896        }
1897    }
1898
1899    #[expect(unsafe_code)]
1900    fn cross_origin_properties(&self) -> &'static CrossOriginProperties {
1901        match self {
1902            WindowOrDissimilarOriginWindow::Window(..) => unsafe {
1903                WindowBinding::CROSS_ORIGIN_PROPERTIES.get()
1904            },
1905            WindowOrDissimilarOriginWindow::DissimilarOriginWindow(..) => unsafe {
1906                DissimilarOriginWindowBinding::CROSS_ORIGIN_PROPERTIES.get()
1907            },
1908        }
1909    }
1910
1911    fn iframe_count(&self) -> u32 {
1912        match self {
1913            WindowOrDissimilarOriginWindow::Window(window) => window.Length(),
1914            WindowOrDissimilarOriginWindow::DissimilarOriginWindow(window) => window.Length(),
1915        }
1916    }
1917
1918    fn window_proxy_for_child_navigable_at_index(
1919        &self,
1920        index: u32,
1921    ) -> Option<DomRoot<WindowProxy>> {
1922        let window_proxy = self.window_proxy();
1923        let browsing_context_id = if let Some(document) = window_proxy.document() {
1924            document.iframes().at_insertion_index(index as usize)?
1925        } else {
1926            let parent_browsing_context_id = window_proxy.browsing_context_id();
1927            let (result_sender, result_receiver) = generic_channel::channel().unwrap();
1928            let _ = self.global_scope().script_to_constellation_chan().send(
1929                ScriptToConstellationMessage::GetChildBrowsingContextId(
1930                    parent_browsing_context_id,
1931                    index as usize,
1932                    result_sender,
1933                ),
1934            );
1935            result_receiver.recv().ok().flatten()?
1936        };
1937        ScriptThread::window_proxies().find_window_proxy(browsing_context_id)
1938    }
1939
1940    /// <https://html.spec.whatwg.org/multipage/#document-tree-child-navigable-target-name-property-set>
1941    ///
1942    /// > The document-tree child navigable target name property set of a Window object window is the
1943    /// > return value of running these steps:
1944    ///
1945    /// > Step 1. Let children be the document-tree child navigables of window's associated Document.
1946    /// > Step 2. Let firstNamedChildren be an empty ordered set.
1947    /// > Step 3. For each navigable of children:
1948    /// > Step 3.1. Let name be navigable's target name.
1949    /// > Step 3.2. If name is the empty string, then continue.
1950    /// > Step 3.3. If firstNamedChildren contains a navigable whose target name is name, then continue.
1951    /// > Step 3.4. Append navigable to firstNamedChildren.
1952    /// > Step 4. Let names be an empty ordered set.
1953    /// > Step 5. For each navigable of firstNamedChildren:
1954    /// > Step 5.1. Let name be navigable's target name.
1955    /// > Step 5.2. If navigable's active document's origin is same origin with window's relevant settings
1956    /// > object's origin, then append name to names.
1957    /// > Step 6. Return names.
1958    ///
1959    /// We don't implement these steps exactly, but we effectively do them using iterators below.
1960    fn named_child_navigable(
1961        &self,
1962        cx: &mut CurrentRealm,
1963        id: HandleId,
1964    ) -> Option<DomRoot<WindowProxy>> {
1965        // TODO: We cannot yet enumerate cross-origin child navigable target names.
1966        let Self::Window(window) = &self else {
1967            return None;
1968        };
1969
1970        let name = jsid_to_string(cx, id)?;
1971        if name.is_empty() {
1972            return None;
1973        }
1974        window
1975            .Document()
1976            .iframes()
1977            .iter()
1978            .filter_map(|iframe| iframe.GetContentWindow())
1979            .find(|window_proxy| window_proxy.get_name() == name)
1980    }
1981}