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