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