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 js::JSCLASS_IS_GLOBAL;
14use js::context::JSContext;
15use js::glue::{
16    CreateWrapperProxyHandler, DeleteWrapperProxyHandler, GetProxyPrivate, GetProxyReservedSlot,
17    ProxyTraps, SetProxyReservedSlot,
18};
19use js::jsapi::{
20    GCContext, Handle as RawHandle, HandleId as RawHandleId, HandleObject as RawHandleObject,
21    HandleValue as RawHandleValue, JS_DefinePropertyById, JS_ForwardSetPropertyTo,
22    JSContext as RawJSContext, JSErrNum, JSObject, JSPROP_ENUMERATE, JSPROP_READONLY, JSTracer,
23    MutableHandle as RawMutableHandle, MutableHandleObject as RawMutableHandleObject,
24    MutableHandleValue as RawMutableHandleValue, ObjectOpResult, PropertyDescriptor,
25};
26use js::jsval::{NullValue, PrivateValue, UndefinedValue};
27use js::realm::{AutoRealm, CurrentRealm};
28use js::rust::wrappers2::{
29    JS_ForwardGetPropertyTo, JS_GetOwnPropertyDescriptorById, JS_HasOwnPropertyById,
30    JS_HasPropertyById, JS_IsExceptionPending, JS_TransplantObject, NewWindowProxy, SetWindowProxy,
31};
32use js::rust::{Handle, MutableHandle, MutableHandleValue, get_object_class};
33use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
34use net_traits::ReferrerPolicy;
35use net_traits::request::Referrer;
36use script_bindings::cell::DomRefCell;
37use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
38use script_bindings::proxyhandler::set_property_descriptor;
39use script_bindings::reflector::{DomObject, MutDomObject, Reflector};
40use script_traits::NewPipelineInfo;
41use serde::{Deserialize, Serialize};
42use servo_base::generic_channel;
43use servo_base::generic_channel::GenericSend;
44use servo_base::id::{BrowsingContextId, PipelineId, WebViewId};
45use servo_constellation_traits::{
46    AuxiliaryWebViewCreationRequest, LoadData, LoadOrigin, NavigationHistoryBehavior,
47    ScriptToConstellationMessage, TargetSnapshotParams,
48};
49use servo_url::{ImmutableOrigin, ServoUrl};
50use storage_traits::webstorage_thread::WebStorageThreadMsg;
51use style::attr::parse_integer;
52
53use crate::dom::bindings::conversions::{ToJSValConvertible, root_from_handleobject};
54use crate::dom::bindings::error::{Error, Fallible, throw_dom_exception};
55use crate::dom::bindings::inheritance::Castable;
56use crate::dom::bindings::reflector::DomGlobal;
57use crate::dom::bindings::root::{Dom, DomRoot};
58use crate::dom::bindings::settings_stack::maybe_entry_global;
59use crate::dom::bindings::str::{DOMString, USVString};
60use crate::dom::bindings::trace::JSTraceable;
61use crate::dom::bindings::utils::get_array_index_from_id;
62use crate::dom::dissimilaroriginwindow::DissimilarOriginWindow;
63use crate::dom::document::Document;
64use crate::dom::element::Element;
65use crate::dom::globalscope::GlobalScope;
66use crate::dom::window::Window;
67use crate::navigation::navigate;
68use crate::script_thread::{ScriptThread, with_script_thread};
69use crate::script_window_proxies::ScriptWindowProxies;
70
71#[dom_struct]
72// NOTE: the browsing context for a window is managed in two places:
73// here, in script, but also in the constellation. The constellation
74// manages the session history, which in script is accessed through
75// History objects, messaging the constellation.
76pub(crate) struct WindowProxy {
77    /// The JS WindowProxy object.
78    /// Unlike other reflectors, we mutate this field because
79    /// we have to brain-transplant the reflector when the WindowProxy
80    /// changes Window.
81    reflector: Reflector,
82
83    /// The id of the browsing context.
84    /// In the case that this is a nested browsing context, this is the id
85    /// of the container.
86    #[no_trace]
87    browsing_context_id: BrowsingContextId,
88
89    // https://html.spec.whatwg.org/multipage/#opener-browsing-context
90    #[no_trace]
91    opener: Option<BrowsingContextId>,
92
93    /// The frame id of the top-level ancestor browsing context.
94    /// In the case that this is a top-level window, this is our id.
95    #[no_trace]
96    webview_id: WebViewId,
97
98    /// The name of the browsing context (sometimes, but not always,
99    /// equal to the name of a container element)
100    name: DomRefCell<DOMString>,
101    /// The pipeline id of the currently active document.
102    /// May be None, when the currently active document is in another script thread.
103    /// We do not try to keep the pipeline id for documents in other threads,
104    /// as this would require the constellation notifying many script threads about
105    /// the change, which could be expensive.
106    #[no_trace]
107    currently_active: Cell<Option<PipelineId>>,
108
109    /// Has the browsing context been discarded?
110    discarded: Cell<bool>,
111
112    /// Has the browsing context been disowned?
113    disowned: Cell<bool>,
114
115    /// <https://html.spec.whatwg.org/multipage/#is-closing>
116    is_closing: Cell<bool>,
117
118    /// If the containing `<iframe>` of this [`WindowProxy`] is from a same-origin page,
119    /// this will be the [`Element`] of the `<iframe>` element in the realm of the
120    /// parent page. Otherwise, it is `None`.
121    frame_element: Option<Dom<Element>>,
122
123    /// The parent browsing context's window proxy, if this is a nested browsing context
124    parent: Option<Dom<WindowProxy>>,
125
126    /// <https://html.spec.whatwg.org/multipage/#delaying-load-events-mode>
127    delaying_load_events_mode: Cell<bool>,
128
129    /// The creator browsing context's url.
130    #[no_trace]
131    creator_url: Option<ServoUrl>,
132
133    /// The creator browsing context's origin.
134    #[no_trace]
135    creator_origin: Option<ImmutableOrigin>,
136
137    /// The window proxies the script thread knows.
138    #[conditional_malloc_size_of]
139    script_window_proxies: Rc<ScriptWindowProxies>,
140}
141
142impl WindowProxy {
143    fn new_inherited(
144        browsing_context_id: BrowsingContextId,
145        webview_id: WebViewId,
146        currently_active: Option<PipelineId>,
147        frame_element: Option<&Element>,
148        parent: Option<&WindowProxy>,
149        opener: Option<BrowsingContextId>,
150        creator: CreatorBrowsingContextInfo,
151    ) -> WindowProxy {
152        let name = frame_element.map_or(DOMString::new(), |e| {
153            e.get_string_attribute(&local_name!("name"))
154        });
155        WindowProxy {
156            reflector: Reflector::new(),
157            browsing_context_id,
158            webview_id,
159            name: DomRefCell::new(name),
160            currently_active: Cell::new(currently_active),
161            discarded: Cell::new(false),
162            disowned: Cell::new(false),
163            is_closing: Cell::new(false),
164            frame_element: frame_element.map(Dom::from_ref),
165            parent: parent.map(Dom::from_ref),
166            delaying_load_events_mode: Cell::new(false),
167            opener,
168            creator_url: creator.url,
169            creator_origin: creator.origin,
170            script_window_proxies: ScriptThread::window_proxies(),
171        }
172    }
173
174    #[expect(unsafe_code)]
175    #[expect(clippy::too_many_arguments)]
176    pub(crate) fn new(
177        cx: &mut JSContext,
178        window: &Window,
179        browsing_context_id: BrowsingContextId,
180        webview_id: WebViewId,
181        frame_element: Option<&Element>,
182        parent: Option<&WindowProxy>,
183        opener: Option<BrowsingContextId>,
184        creator: CreatorBrowsingContextInfo,
185    ) -> DomRoot<WindowProxy> {
186        unsafe {
187            let handler = window.windowproxy_handler();
188
189            let window_jsobject = window.reflector().get_jsobject();
190            assert!(!window_jsobject.get().is_null());
191            assert_ne!(
192                ((*get_object_class(window_jsobject.get())).flags & JSCLASS_IS_GLOBAL),
193                0
194            );
195
196            let mut realm = AutoRealm::new_from_handle(cx, window_jsobject);
197            let cx = &mut realm;
198
199            // Create a new window proxy.
200            rooted!(&in(cx) let js_proxy = handler.new_window_proxy(cx, window_jsobject));
201            assert!(!js_proxy.is_null());
202
203            // Create a new browsing context.
204
205            let current = Some(window.upcast::<GlobalScope>().pipeline_id());
206            let window_proxy = Box::new(WindowProxy::new_inherited(
207                browsing_context_id,
208                webview_id,
209                current,
210                frame_element,
211                parent,
212                opener,
213                creator,
214            ));
215
216            // The window proxy owns the browsing context.
217            // When we finalize the window proxy, it drops the browsing context it owns.
218            SetProxyReservedSlot(
219                js_proxy.get(),
220                0,
221                &PrivateValue(&raw const (*window_proxy) as *const libc::c_void),
222            );
223
224            // Notify the JS engine about the new window proxy binding.
225            SetWindowProxy(cx, window_jsobject, js_proxy.handle());
226
227            // Set the reflector.
228            debug!(
229                "Initializing reflector of {:p} to {:p}.",
230                window_proxy,
231                js_proxy.get()
232            );
233            window_proxy
234                .reflector
235                .init_reflector::<WindowProxy>(js_proxy.get());
236            DomRoot::from_ref(&*Box::into_raw(window_proxy))
237        }
238    }
239
240    #[expect(unsafe_code)]
241    pub(crate) fn new_dissimilar_origin(
242        cx: &mut JSContext,
243        global_to_clone_from: &GlobalScope,
244        browsing_context_id: BrowsingContextId,
245        webview_id: WebViewId,
246        parent: Option<&WindowProxy>,
247        opener: Option<BrowsingContextId>,
248        creator: CreatorBrowsingContextInfo,
249    ) -> DomRoot<WindowProxy> {
250        unsafe {
251            let handler = WindowProxyHandler::x_origin_proxy_handler();
252
253            // Create a new browsing context.
254            let window_proxy = Box::new(WindowProxy::new_inherited(
255                browsing_context_id,
256                webview_id,
257                None,
258                None,
259                parent,
260                opener,
261                creator,
262            ));
263
264            // Create a new dissimilar-origin window.
265            let window = DissimilarOriginWindow::new(cx, global_to_clone_from, &window_proxy);
266            let window_jsobject = window.reflector().get_jsobject();
267            assert!(!window_jsobject.get().is_null());
268            assert_ne!(
269                ((*get_object_class(window_jsobject.get())).flags & JSCLASS_IS_GLOBAL),
270                0
271            );
272
273            let mut realm = AutoRealm::new_from_handle(cx, window_jsobject);
274            let cx = &mut realm;
275
276            // Create a new window proxy.
277            rooted!(&in(cx) let js_proxy = handler.new_window_proxy(cx, window_jsobject));
278            assert!(!js_proxy.is_null());
279
280            // The window proxy owns the browsing context.
281            // When we finalize the window proxy, it drops the browsing context it owns.
282            SetProxyReservedSlot(
283                js_proxy.get(),
284                0,
285                &PrivateValue(&raw const (*window_proxy) as *const libc::c_void),
286            );
287
288            // Notify the JS engine about the new window proxy binding.
289            SetWindowProxy(cx, window_jsobject, js_proxy.handle());
290
291            // Set the reflector.
292            debug!(
293                "Initializing reflector of {:p} to {:p}.",
294                window_proxy,
295                js_proxy.get()
296            );
297            window_proxy
298                .reflector
299                .init_reflector::<WindowProxy>(js_proxy.get());
300            DomRoot::from_ref(&*Box::into_raw(window_proxy))
301        }
302    }
303
304    /// <https://html.spec.whatwg.org/multipage/#auxiliary-browsing-context>
305    fn create_auxiliary_browsing_context(
306        &self,
307        cx: &mut JSContext,
308        name: DOMString,
309        noopener: bool,
310    ) -> Option<DomRoot<WindowProxy>> {
311        let (response_sender, response_receiver) = generic_channel::channel().unwrap();
312        let window = self
313            .currently_active
314            .get()
315            .and_then(ScriptThread::find_document)
316            .map(|doc| DomRoot::from_ref(doc.window()))
317            .unwrap();
318
319        let document = self
320            .currently_active
321            .get()
322            .and_then(ScriptThread::find_document)
323            .expect("A WindowProxy creating an auxiliary to have an active document");
324
325        // <https://html.spec.whatwg.org/multipage/#navigable-target-names>
326        // > If the user agent has been configured such that in this instance it
327        // > will create a new top-level traversable
328        // >
329        // Step 9. If sandboxingFlagSet's sandbox propagates to auxiliary browsing
330        //   contexts flag is set, then all the flags that are set in sandboxingFlagSet
331        // must be set in chosen's active browsing context's popup sandboxing flag set.
332        let sandboxing_flag_set = document.active_sandboxing_flag_set();
333        let propagate_sandbox = sandboxing_flag_set
334            .contains(SandboxingFlagSet::SANDBOX_PROPOGATES_TO_AUXILIARY_BROWSING_CONTEXTS_FLAG);
335        let sandboxing_flag_set = if propagate_sandbox {
336            sandboxing_flag_set
337        } else {
338            SandboxingFlagSet::empty()
339        };
340
341        let blank_url = ServoUrl::parse("about:blank").ok().unwrap();
342        let mut load_data = LoadData::new(
343            LoadOrigin::Script(document.origin().snapshot()),
344            blank_url,
345            Some(document.base_url()),
346            // This has the effect of ensuring that the new `about:blank` URL has the
347            // same origin as the `Document` that is creating the new browsing context.
348            Some(window.pipeline_id()),
349            document.global().get_referrer(),
350            document.get_referrer_policy(),
351            None, // Doesn't inherit secure context
352            None,
353            false,
354            sandboxing_flag_set,
355        );
356        load_data.is_initial_about_blank = true;
357        let load_info = AuxiliaryWebViewCreationRequest {
358            load_data: load_data.clone(),
359            opener_webview_id: window.webview_id(),
360            opener_pipeline_id: self.currently_active.get().unwrap(),
361            response_sender,
362        };
363        let constellation_msg = ScriptToConstellationMessage::CreateAuxiliaryWebView(load_info);
364        window.send_to_constellation(constellation_msg);
365
366        let response = response_receiver.recv().unwrap()?;
367        let new_browsing_context_id = BrowsingContextId::from(response.new_webview_id);
368        let new_pipeline_info = NewPipelineInfo {
369            parent_info: None,
370            new_pipeline_id: response.new_pipeline_id,
371            browsing_context_id: new_browsing_context_id,
372            webview_id: response.new_webview_id,
373            opener: Some(self.browsing_context_id),
374            load_data,
375            viewport_details: window.viewport_details(),
376            user_content_manager_id: response.user_content_manager_id,
377            // Use the current `WebView`'s theme initially, but the embedder may
378            // change this later.
379            theme: window.theme(),
380            target_snapshot_params: TargetSnapshotParams {
381                sandboxing_flags: sandboxing_flag_set,
382                iframe_element_referrer_policy: ReferrerPolicy::EmptyString,
383            },
384        };
385
386        with_script_thread(|script_thread| {
387            script_thread.spawn_pipeline(cx, new_pipeline_info);
388        });
389
390        let new_window_proxy = ScriptThread::find_document(response.new_pipeline_id)
391            .and_then(|doc| doc.browsing_context())?;
392        if name.to_lowercase() != "_blank" {
393            new_window_proxy.set_name(name);
394        }
395        if noopener {
396            new_window_proxy.disown();
397        } else {
398            // After creating a new auxiliary browsing context and document,
399            // the session storage is copied over.
400            // See https://html.spec.whatwg.org/multipage/#the-sessionstorage-attribute
401
402            let (sender, receiver) = generic_channel::channel().unwrap();
403
404            let msg = WebStorageThreadMsg::Clone {
405                sender,
406                src: window.window_proxy().webview_id(),
407                dest: response.new_webview_id,
408            };
409
410            GenericSend::send(document.global().storage_threads(), msg).unwrap();
411            receiver.recv().unwrap();
412        }
413        Some(new_window_proxy)
414    }
415
416    /// <https://html.spec.whatwg.org/multipage/#delaying-load-events-mode>
417    pub(crate) fn is_delaying_load_events_mode(&self) -> bool {
418        self.delaying_load_events_mode.get()
419    }
420
421    /// <https://html.spec.whatwg.org/multipage/#delaying-load-events-mode>
422    pub(crate) fn start_delaying_load_events_mode(&self) {
423        self.delaying_load_events_mode.set(true);
424    }
425
426    /// <https://html.spec.whatwg.org/multipage/#delaying-load-events-mode>
427    pub(crate) fn stop_delaying_load_events_mode(&self) {
428        self.delaying_load_events_mode.set(false);
429        if let Some(document) = self.document() &&
430            !document.loader().events_inhibited()
431        {
432            ScriptThread::mark_document_with_no_blocked_loads(&document);
433        }
434    }
435
436    // https://html.spec.whatwg.org/multipage/#disowned-its-opener
437    pub(crate) fn disown(&self) {
438        self.disowned.set(true);
439    }
440
441    /// <https://html.spec.whatwg.org/multipage/#dom-window-close>
442    /// Step 3.1, set BCs `is_closing` to true.
443    pub(crate) fn close(&self) {
444        self.is_closing.set(true);
445    }
446
447    /// <https://html.spec.whatwg.org/multipage/#is-closing>
448    pub(crate) fn is_closing(&self) -> bool {
449        self.is_closing.get()
450    }
451
452    // https://html.spec.whatwg.org/multipage/#dom-opener
453    pub(crate) fn opener(&self, cx: &mut CurrentRealm, mut retval: MutableHandleValue) {
454        if self.disowned.get() {
455            return retval.set(NullValue());
456        }
457        let opener_id = match self.opener {
458            Some(opener_browsing_context_id) => opener_browsing_context_id,
459            None => return retval.set(NullValue()),
460        };
461        let parent_browsing_context = self.parent.as_deref();
462        let opener_proxy = match self.script_window_proxies.find_window_proxy(opener_id) {
463            Some(window_proxy) => window_proxy,
464            None => {
465                let sender_pipeline_id = self.currently_active().unwrap();
466                match ScriptThread::get_top_level_for_browsing_context(
467                    self.webview_id(),
468                    sender_pipeline_id,
469                    opener_id,
470                ) {
471                    Some(opener_top_id) => {
472                        let global_to_clone_from = GlobalScope::from_current_realm(cx);
473                        let creator =
474                            CreatorBrowsingContextInfo::from(parent_browsing_context, None);
475                        WindowProxy::new_dissimilar_origin(
476                            cx,
477                            &global_to_clone_from,
478                            opener_id,
479                            opener_top_id,
480                            None,
481                            None,
482                            creator,
483                        )
484                    },
485                    None => return retval.set(NullValue()),
486                }
487            },
488        };
489        if opener_proxy.is_browsing_context_discarded() {
490            return retval.set(NullValue());
491        }
492        opener_proxy.safe_to_jsval(cx, retval);
493    }
494
495    // https://html.spec.whatwg.org/multipage/#window-open-steps
496    pub(crate) fn open(
497        &self,
498        cx: &mut JSContext,
499        url: USVString,
500        target: DOMString,
501        features: DOMString,
502    ) -> Fallible<Option<DomRoot<WindowProxy>>> {
503        // Note: this does not map to the spec,
504        // but it does prevent a panic at the constellation because the browsing context
505        // has already been discarded.
506        // See issue: #39716 for the original problem,
507        // and https://github.com/whatwg/html/issues/11797 for a discussion at the level of the spec.
508        if self.discarded.get() {
509            return Ok(None);
510        }
511        // Step 2. Let sourceDocument be the entry global object's associated Document.
512        //
513        // It's possible to end up in a situation where JS code is executing but
514        // we have not had a chance to push an entry global (e.g. via WASM instantiation).
515        // If that happens, fall back to the active document of this browsing context.
516        let source_document = maybe_entry_global()
517            .map(|global| global.as_window().Document())
518            .or_else(|| self.document())
519            .expect("Must have an entry global or active document");
520        // Step 4. If url is not the empty string:
521        let url_record = if !url.is_empty() {
522            // Step 4.1. Set urlRecord to the result of encoding-parsing a URL given url, relative to sourceDocument.
523            let Ok(url) = source_document.encoding_parse_a_url(&url) else {
524                // Step 4.2. If urlRecord is failure, then throw a "SyntaxError" DOMException.
525                return Err(Error::Syntax(Some(format!(
526                    "Error parsing URL '{url}' relative to '{}'",
527                    source_document.url()
528                ))));
529            };
530            Some(url)
531        } else {
532            // Step 3. Let urlRecord be null.
533            None
534        };
535        // Step 5. If target is the empty string, then set target to "_blank".
536        let non_empty_target = if target.is_empty() {
537            DOMString::from("_blank")
538        } else {
539            target
540        };
541        // Step 6. Let tokenizedFeatures be the result of tokenizing features.
542        let tokenized_features = tokenize_open_features(features);
543        // Step 7 - 8.
544        // If tokenizedFeatures["noreferrer"] exists, then set noreferrer to
545        // the result of parsing tokenizedFeatures["noreferrer"] as a boolean feature.
546        let noreferrer = parse_open_feature_boolean(&tokenized_features, "noreferrer");
547
548        // Step 9. Let noopener be the result of getting noopener for window
549        // open with sourceDocument, tokenizedFeatures, and urlRecord.
550        let noopener = if noreferrer {
551            true
552        } else {
553            parse_open_feature_boolean(&tokenized_features, "noopener")
554        };
555        // (TODO) Step 10. Remove tokenizedFeatures["noopener"] and tokenizedFeatures["noreferrer"].
556
557        // (TODO) Step 11. Let referrerPolicy be the empty string.
558        // (TODO) Step 12. If noreferrer is true, then set referrerPolicy to "no-referrer".
559
560        // Step 13 - 14
561        // Let targetNavigable and windowType be the result of applying the rules for
562        // choosing a navigable given target, sourceDocument's node navigable, and noopener.
563        // If targetNavigable is null, then return null.
564        let (chosen, new) = match self.choose_browsing_context(cx, non_empty_target, noopener) {
565            (Some(chosen), new) => (chosen, new),
566            (None, _) => return Ok(None),
567        };
568        // TODO Step 15.2, Set up browsing context features for targetNavigable's
569        // active browsing context given tokenizedFeatures.
570        let target_document = match chosen.document() {
571            Some(target_document) => target_document,
572            None => return Ok(None),
573        };
574        let has_trustworthy_ancestor_origin = if new {
575            target_document.has_trustworthy_ancestor_or_current_origin()
576        } else {
577            false
578        };
579        let target_window = target_document.window();
580        // Step 15.3. If urlRecord is null, then set urlRecord to a URL record representing about:blank.
581        let url_record = url_record.unwrap_or(ServoUrl::parse("about:blank").unwrap());
582        // Step 15.4. If urlRecord matches about:blank, then perform the URL and history update steps given targetNavigable's active document and urlRecord.
583        //
584        // This happened in the constellation as part of creating the auxiliary browsing context.
585        if !url_record.matches_about_blank() {
586            let referrer = if noreferrer {
587                Referrer::NoReferrer
588            } else {
589                target_window.as_global_scope().get_referrer()
590            };
591            // Propagate CSP list and about-base-url from opener to new document
592            let csp_list = source_document.get_csp_list().clone();
593            target_document.set_csp_list(csp_list);
594
595            // Step 15.5 Otherwise, navigate targetNavigable to urlRecord using sourceDocument,
596            // with referrerPolicy set to referrerPolicy and exceptionsEnabled set to true.
597            // FIXME: referrerPolicy may not be used properly here. exceptionsEnabled not used.
598            let mut load_data = LoadData::new(
599                LoadOrigin::Script(source_document.origin().snapshot()),
600                url_record,
601                target_document.about_base_url(),
602                Some(target_window.pipeline_id()),
603                referrer,
604                target_document.get_referrer_policy(),
605                Some(target_window.as_global_scope().is_secure_context()),
606                Some(target_document.insecure_requests_policy()),
607                has_trustworthy_ancestor_origin,
608                target_document.creation_sandboxing_flag_set_considering_parent_iframe(),
609            );
610
611            // Handle javascript: URLs specially to report CSP violations to the source window
612            // https://html.spec.whatwg.org/multipage/#navigate-to-a-javascript:-url
613            if load_data.url.scheme() == "javascript" {
614                let existing_global = source_document.global();
615
616                // Check CSP and report violations to the source (existing) window
617                if !ScriptThread::can_navigate_to_javascript_url(
618                    cx,
619                    &existing_global,
620                    target_window.as_global_scope(),
621                    &mut load_data,
622                    None,
623                ) {
624                    // CSP blocked the navigation, don't proceed
625                    return Ok(target_document.browsing_context());
626                }
627            }
628
629            let history_handling = if new {
630                NavigationHistoryBehavior::Replace
631            } else {
632                NavigationHistoryBehavior::Push
633            };
634            navigate(cx, target_window, history_handling, false, load_data);
635        }
636        // Step 17 (Dis-owning has been done in create_auxiliary_browsing_context).
637        if noopener {
638            return Ok(None);
639        }
640        // Step 18
641        Ok(target_document.browsing_context())
642    }
643
644    // https://html.spec.whatwg.org/multipage/#the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name
645    pub(crate) fn choose_browsing_context(
646        &self,
647        cx: &mut JSContext,
648        name: DOMString,
649        noopener: bool,
650    ) -> (Option<DomRoot<WindowProxy>>, bool) {
651        match name.to_lowercase().as_ref() {
652            "" | "_self" => {
653                // Step 3.
654                (Some(DomRoot::from_ref(self)), false)
655            },
656            "_parent" => {
657                // Step 4
658                if let Some(parent) = self.parent() {
659                    return (Some(DomRoot::from_ref(parent)), false);
660                }
661                (None, false)
662            },
663            "_top" => {
664                // Step 5
665                (Some(DomRoot::from_ref(self.top())), false)
666            },
667            "_blank" => (
668                self.create_auxiliary_browsing_context(cx, name, noopener),
669                true,
670            ),
671            _ => {
672                // Step 6.
673                // TODO: expand the search to all 'familiar' bc,
674                // including auxiliaries familiar by way of their opener.
675                // See https://html.spec.whatwg.org/multipage/#familiar-with
676                match ScriptThread::find_window_proxy_by_name(&name) {
677                    Some(proxy) => (Some(proxy), false),
678                    None => (
679                        self.create_auxiliary_browsing_context(cx, name, noopener),
680                        true,
681                    ),
682                }
683            },
684        }
685    }
686
687    pub(crate) fn is_auxiliary(&self) -> bool {
688        self.opener.is_some()
689    }
690
691    pub(crate) fn discard_browsing_context(&self) {
692        self.discarded.set(true);
693    }
694
695    pub(crate) fn is_browsing_context_discarded(&self) -> bool {
696        self.discarded.get()
697    }
698
699    pub(crate) fn browsing_context_id(&self) -> BrowsingContextId {
700        self.browsing_context_id
701    }
702
703    pub(crate) fn webview_id(&self) -> WebViewId {
704        self.webview_id
705    }
706
707    /// If the containing `<iframe>` of this [`WindowProxy`] is from a same-origin page,
708    /// this will return an [`Element`] of the `<iframe>` element in the realm of the parent
709    /// page.
710    pub(crate) fn frame_element(&self) -> Option<&Element> {
711        self.frame_element.as_deref()
712    }
713
714    pub(crate) fn document(&self) -> Option<DomRoot<Document>> {
715        self.currently_active
716            .get()
717            .and_then(ScriptThread::find_document)
718    }
719
720    pub(crate) fn parent(&self) -> Option<&WindowProxy> {
721        self.parent.as_deref()
722    }
723
724    pub(crate) fn top(&self) -> &WindowProxy {
725        let mut result = self;
726        while let Some(parent) = result.parent() {
727            result = parent;
728        }
729        result
730    }
731
732    pub fn document_origin(&self) -> Option<String> {
733        let pipeline_id = self.currently_active()?;
734        let (result_sender, result_receiver) = generic_channel::channel().unwrap();
735        self.global()
736            .script_to_constellation_chan()
737            .send(ScriptToConstellationMessage::GetDocumentOrigin(
738                pipeline_id,
739                result_sender,
740            ))
741            .ok()?;
742        result_receiver.recv().ok()?
743    }
744
745    #[expect(unsafe_code)]
746    /// Change the Window that this WindowProxy resolves to.
747    // TODO: support setting the window proxy to a dummy value,
748    // to handle the case when the active document is in another script thread.
749    fn set_window(&self, cx: &mut JSContext, window: &GlobalScope, handler: &WindowProxyHandler) {
750        unsafe {
751            debug!("Setting window of {:p}.", self);
752
753            let window_jsobject = window.reflector().get_jsobject();
754            let old_js_proxy = self.reflector.get_jsobject();
755            assert!(!window_jsobject.get().is_null());
756            assert_ne!(
757                ((*get_object_class(window_jsobject.get())).flags & JSCLASS_IS_GLOBAL),
758                0
759            );
760
761            let mut realm = AutoRealm::new_from_handle(cx, window_jsobject);
762            let cx = &mut realm;
763
764            // The old window proxy no longer owns this browsing context.
765            SetProxyReservedSlot(old_js_proxy.get(), 0, &PrivateValue(ptr::null_mut()));
766
767            // Brain transplant the window proxy. Brain transplantation is
768            // usually done to move a window proxy between compartments, but
769            // that's not what we are doing here. We need to do this just
770            // because we want to replace the wrapper's `ProxyTraps`, but we
771            // don't want to update its identity.
772            rooted!(&in(cx) let new_js_proxy = handler.new_window_proxy(cx, window_jsobject));
773            // Explicitly set this slot to a null pointer in case a GC occurs before we
774            // are ready to set it to a real value.
775            SetProxyReservedSlot(new_js_proxy.get(), 0, &PrivateValue(ptr::null_mut()));
776            debug!(
777                "Transplanting proxy from {:p} to {:p}.",
778                old_js_proxy.get(),
779                new_js_proxy.get()
780            );
781            rooted!(&in(cx) let new_js_proxy = JS_TransplantObject(cx, old_js_proxy, new_js_proxy.handle()));
782            debug!("Transplanted proxy is {:p}.", new_js_proxy.get());
783
784            // Transfer ownership of this browsing context from the old window proxy to the new one.
785            SetProxyReservedSlot(
786                new_js_proxy.get(),
787                0,
788                &PrivateValue(self as *const _ as *const libc::c_void),
789            );
790
791            // Notify the JS engine about the new window proxy binding.
792            SetWindowProxy(cx, window_jsobject, new_js_proxy.handle());
793
794            // Update the reflector.
795            debug!(
796                "Setting reflector of {:p} to {:p}.",
797                self,
798                new_js_proxy.get()
799            );
800            self.reflector.rootable().set(new_js_proxy.get());
801        }
802    }
803
804    pub(crate) fn set_currently_active(&self, cx: &mut JSContext, window: &Window) {
805        if let Some(pipeline_id) = self.currently_active() &&
806            pipeline_id == window.pipeline_id()
807        {
808            return debug!(
809                "Attempt to set the currently active window to the currently active window."
810            );
811        }
812
813        let global_scope = window.as_global_scope();
814        self.set_window(cx, global_scope, WindowProxyHandler::proxy_handler());
815        self.currently_active.set(Some(global_scope.pipeline_id()));
816    }
817
818    pub(crate) fn unset_currently_active(&self, cx: &mut JSContext) {
819        if self.currently_active().is_none() {
820            return debug!(
821                "Attempt to unset the currently active window on a windowproxy that does not have one."
822            );
823        }
824        let globalscope = self.global();
825        let window = DissimilarOriginWindow::new(cx, &globalscope, self);
826        self.set_window(
827            cx,
828            window.upcast(),
829            WindowProxyHandler::x_origin_proxy_handler(),
830        );
831        self.currently_active.set(None);
832    }
833
834    pub(crate) fn currently_active(&self) -> Option<PipelineId> {
835        self.currently_active.get()
836    }
837
838    pub(crate) fn get_name(&self) -> DOMString {
839        self.name.borrow().clone()
840    }
841
842    pub(crate) fn set_name(&self, name: DOMString) {
843        *self.name.borrow_mut() = name;
844    }
845}
846
847/// A browsing context can have a creator browsing context, the browsing context that
848/// was responsible for its creation. If a browsing context has a parent browsing context,
849/// then that is its creator browsing context. Otherwise, if the browsing context has an
850/// opener browsing context, then that is its creator browsing context. Otherwise, the
851/// browsing context has no creator browsing context.
852///
853/// If a browsing context A has a creator browsing context, then the Document that was the
854/// active document of that creator browsing context at the time A was created is the creator
855/// Document.
856///
857/// See: <https://html.spec.whatwg.org/multipage/#creating-browsing-contexts>
858#[derive(Debug, Deserialize, Serialize)]
859pub(crate) struct CreatorBrowsingContextInfo {
860    /// Creator document URL.
861    url: Option<ServoUrl>,
862
863    /// Creator document origin.
864    origin: Option<ImmutableOrigin>,
865}
866
867impl CreatorBrowsingContextInfo {
868    pub(crate) fn from(
869        parent: Option<&WindowProxy>,
870        opener: Option<&WindowProxy>,
871    ) -> CreatorBrowsingContextInfo {
872        let creator = match (parent, opener) {
873            (Some(parent), _) => parent.document(),
874            (None, Some(opener)) => opener.document(),
875            (None, None) => None,
876        };
877
878        let url = creator.as_deref().map(|document| document.url());
879        let origin = creator
880            .as_deref()
881            .map(|document| document.origin().immutable().clone());
882
883        CreatorBrowsingContextInfo { url, origin }
884    }
885}
886
887/// <https://html.spec.whatwg.org/multipage/#concept-window-open-features-tokenize>
888fn tokenize_open_features(features: DOMString) -> IndexMap<String, String> {
889    let is_feature_sep = |c: char| c.is_ascii_whitespace() || ['=', ','].contains(&c);
890    // Step 1
891    let mut tokenized_features = IndexMap::new();
892    // Step 2
893    let features = features.str();
894    let mut iter = features.chars();
895    let mut cur = iter.next();
896
897    // Step 3
898    while cur.is_some() {
899        // Step 3.1 & 3.2
900        let mut name = String::new();
901        let mut value = String::new();
902        // Step 3.3
903        while let Some(cur_char) = cur {
904            if !is_feature_sep(cur_char) {
905                break;
906            }
907            cur = iter.next();
908        }
909        // Step 3.4
910        while let Some(cur_char) = cur {
911            if is_feature_sep(cur_char) {
912                break;
913            }
914            name.push(cur_char.to_ascii_lowercase());
915            cur = iter.next();
916        }
917        // Step 3.5
918        let normalized_name = String::from(match name.as_ref() {
919            "screenx" => "left",
920            "screeny" => "top",
921            "innerwidth" => "width",
922            "innerheight" => "height",
923            _ => name.as_ref(),
924        });
925        // Step 3.6
926        while let Some(cur_char) = cur {
927            if cur_char == '=' || cur_char == ',' || !is_feature_sep(cur_char) {
928                break;
929            }
930            cur = iter.next();
931        }
932        // Step 3.7
933        if cur.is_some() && is_feature_sep(cur.unwrap()) {
934            // Step 3.7.1
935            while let Some(cur_char) = cur {
936                if !is_feature_sep(cur_char) || cur_char == ',' {
937                    break;
938                }
939                cur = iter.next();
940            }
941            // Step 3.7.2
942            while let Some(cur_char) = cur {
943                if is_feature_sep(cur_char) {
944                    break;
945                }
946                value.push(cur_char.to_ascii_lowercase());
947                cur = iter.next();
948            }
949        }
950        // Step 3.8
951        if !name.is_empty() {
952            tokenized_features.insert(normalized_name, value);
953        }
954    }
955    // Step 4
956    tokenized_features
957}
958
959/// <https://html.spec.whatwg.org/multipage/#concept-window-open-features-parse-boolean>
960fn parse_open_feature_boolean(tokenized_features: &IndexMap<String, String>, name: &str) -> bool {
961    if let Some(value) = tokenized_features.get(name) {
962        // Step 1 & 2
963        if value.is_empty() || value == "yes" {
964            return true;
965        }
966        // Step 3 & 4
967        if let Ok(int) = parse_integer(value.chars()) {
968            return int != 0;
969        }
970    }
971    // Step 5
972    false
973}
974
975// This is only called from extern functions,
976// there's no use using the lifetimed handles here.
977// https://html.spec.whatwg.org/multipage/#accessing-other-browsing-contexts
978#[expect(unsafe_code)]
979#[expect(non_snake_case)]
980unsafe fn GetSubframeWindowProxy(
981    cx: &mut JSContext,
982    proxy: RawHandleObject,
983    id: RawHandleId,
984) -> Option<(DomRoot<WindowProxy>, u32)> {
985    let index = get_array_index_from_id(unsafe { Handle::from_raw(id) });
986    if let Some(index) = index {
987        let mut slot = UndefinedValue();
988        unsafe { GetProxyPrivate(*proxy, &mut slot) };
989        rooted!(&in(cx) let target = slot.to_object());
990        let script_window_proxies = ScriptThread::window_proxies();
991        if let Ok(win) = root_from_handleobject::<Window>(cx, target.handle()) {
992            let browsing_context_id = win.window_proxy().browsing_context_id();
993            let (result_sender, result_receiver) = generic_channel::channel().unwrap();
994
995            let _ = win.as_global_scope().script_to_constellation_chan().send(
996                ScriptToConstellationMessage::GetChildBrowsingContextId(
997                    browsing_context_id,
998                    index as usize,
999                    result_sender,
1000                ),
1001            );
1002            return result_receiver
1003                .recv()
1004                .ok()
1005                .and_then(|maybe_bcid| maybe_bcid)
1006                .and_then(|id| script_window_proxies.find_window_proxy(id))
1007                .map(|proxy| (proxy, (JSPROP_ENUMERATE | JSPROP_READONLY) as u32));
1008        } else if let Ok(win) =
1009            root_from_handleobject::<DissimilarOriginWindow>(cx, target.handle())
1010        {
1011            let browsing_context_id = win.window_proxy().browsing_context_id();
1012            let (result_sender, result_receiver) = generic_channel::channel().unwrap();
1013
1014            let _ = win.global().script_to_constellation_chan().send(
1015                ScriptToConstellationMessage::GetChildBrowsingContextId(
1016                    browsing_context_id,
1017                    index as usize,
1018                    result_sender,
1019                ),
1020            );
1021            return result_receiver
1022                .recv()
1023                .ok()
1024                .and_then(|maybe_bcid| maybe_bcid)
1025                .and_then(|id| script_window_proxies.find_window_proxy(id))
1026                .map(|proxy| (proxy, JSPROP_READONLY as u32));
1027        }
1028    }
1029
1030    None
1031}
1032
1033#[expect(unsafe_code)]
1034unsafe extern "C" fn get_own_property_descriptor(
1035    cx: *mut RawJSContext,
1036    proxy: RawHandleObject,
1037    id: RawHandleId,
1038    desc: RawMutableHandle<PropertyDescriptor>,
1039    is_none: *mut bool,
1040) -> bool {
1041    let mut cx = unsafe {
1042        // SAFETY: We are in SM hook
1043        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1044    };
1045    let cx = &mut cx;
1046    let window = unsafe { GetSubframeWindowProxy(cx, proxy, id) };
1047    let desc = unsafe { MutableHandle::from_raw(desc) };
1048    if let Some((window, attrs)) = window {
1049        rooted!(&in(cx) let mut val = UndefinedValue());
1050        window.safe_to_jsval(cx, val.handle_mut());
1051        set_property_descriptor(desc, val.handle(), attrs, unsafe { &mut *is_none });
1052        return true;
1053    }
1054
1055    let mut slot = UndefinedValue();
1056    unsafe { GetProxyPrivate(proxy.get(), &mut slot) };
1057    rooted!(&in(cx) let target = slot.to_object());
1058    unsafe {
1059        JS_GetOwnPropertyDescriptorById(cx, target.handle(), Handle::from_raw(id), desc, is_none)
1060    }
1061}
1062
1063#[expect(unsafe_code)]
1064unsafe extern "C" fn define_property(
1065    cx: *mut RawJSContext,
1066    proxy: RawHandleObject,
1067    id: RawHandleId,
1068    desc: RawHandle<PropertyDescriptor>,
1069    res: *mut ObjectOpResult,
1070) -> bool {
1071    if get_array_index_from_id(unsafe { Handle::from_raw(id) }).is_some() {
1072        // Spec says to Reject whether this is a supported index or not,
1073        // since we have no indexed setter or indexed creator.  That means
1074        // throwing in strict mode (FIXME: Bug 828137), doing nothing in
1075        // non-strict mode.
1076        unsafe {
1077            (*res).code_ = JSErrNum::JSMSG_CANT_DEFINE_WINDOW_ELEMENT as ::libc::uintptr_t;
1078        }
1079        return true;
1080    }
1081
1082    let mut slot = UndefinedValue();
1083    unsafe { GetProxyPrivate(*proxy.ptr, &mut slot) };
1084    rooted!(in(cx) let target = slot.to_object());
1085    unsafe { JS_DefinePropertyById(cx, target.handle().into(), id, desc, res) }
1086}
1087
1088#[expect(unsafe_code)]
1089unsafe extern "C" fn has(
1090    cx: *mut RawJSContext,
1091    proxy: RawHandleObject,
1092    id: RawHandleId,
1093    bp: *mut bool,
1094) -> bool {
1095    let mut cx = unsafe {
1096        // SAFETY: We are in SM hook
1097        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1098    };
1099    let cx = &mut cx;
1100    let window = unsafe { GetSubframeWindowProxy(cx, proxy, id) };
1101    if window.is_some() {
1102        unsafe { *bp = true };
1103        return true;
1104    }
1105
1106    let mut slot = UndefinedValue();
1107    unsafe { GetProxyPrivate(*proxy.ptr, &mut slot) };
1108    rooted!(&in(cx) let target = slot.to_object());
1109    let mut found = false;
1110    if !unsafe { JS_HasPropertyById(cx, target.handle(), Handle::from_raw(id), &mut found) } {
1111        return false;
1112    }
1113
1114    unsafe { *bp = found };
1115    true
1116}
1117
1118#[expect(unsafe_code)]
1119unsafe extern "C" fn get(
1120    cx: *mut RawJSContext,
1121    proxy: RawHandleObject,
1122    receiver: RawHandleValue,
1123    id: RawHandleId,
1124    vp: RawMutableHandleValue,
1125) -> bool {
1126    let mut cx = unsafe {
1127        // SAFETY: We are in SM hook
1128        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1129    };
1130    let cx = &mut cx;
1131    let window = unsafe { GetSubframeWindowProxy(cx, proxy, id) };
1132    let vp = unsafe { MutableHandle::from_raw(vp) };
1133    if let Some((window, _attrs)) = window {
1134        window.safe_to_jsval(cx, vp);
1135        return true;
1136    }
1137
1138    let mut slot = UndefinedValue();
1139    unsafe { GetProxyPrivate(*proxy.ptr, &mut slot) };
1140    rooted!(&in(cx) let target = slot.to_object());
1141    unsafe {
1142        JS_ForwardGetPropertyTo(
1143            cx,
1144            target.handle(),
1145            Handle::from_raw(id),
1146            Handle::from_raw(receiver),
1147            vp,
1148        )
1149    }
1150}
1151
1152#[expect(unsafe_code)]
1153unsafe extern "C" fn set(
1154    cx: *mut RawJSContext,
1155    proxy: RawHandleObject,
1156    id: RawHandleId,
1157    v: RawHandleValue,
1158    receiver: RawHandleValue,
1159    res: *mut ObjectOpResult,
1160) -> bool {
1161    if get_array_index_from_id(unsafe { Handle::from_raw(id) }).is_some() {
1162        // Reject (which means throw if and only if strict) the set.
1163        unsafe { (*res).code_ = JSErrNum::JSMSG_READ_ONLY as ::libc::uintptr_t };
1164        return true;
1165    }
1166
1167    let mut slot = UndefinedValue();
1168    unsafe { GetProxyPrivate(*proxy.ptr, &mut slot) };
1169    rooted!(in(cx) let target = slot.to_object());
1170    unsafe { JS_ForwardSetPropertyTo(cx, target.handle().into(), id, v, receiver, res) }
1171}
1172
1173#[expect(unsafe_code)]
1174unsafe extern "C" fn get_prototype_if_ordinary(
1175    _: *mut RawJSContext,
1176    _: RawHandleObject,
1177    is_ordinary: *mut bool,
1178    _: RawMutableHandleObject,
1179) -> bool {
1180    // Window's [[GetPrototypeOf]] trap isn't the ordinary definition:
1181    //
1182    //   https://html.spec.whatwg.org/multipage/#windowproxy-getprototypeof
1183    //
1184    // We nonetheless can implement it with a static [[Prototype]], because
1185    // wrapper-class handlers (particularly, XOW in FilteringWrapper.cpp) supply
1186    // all non-ordinary behavior.
1187    //
1188    // But from a spec point of view, it's the exact same object in both cases --
1189    // only the observer's changed.  So this getPrototypeIfOrdinary trap on the
1190    // non-wrapper object *must* report non-ordinary, even if static [[Prototype]]
1191    // usually means ordinary.
1192    unsafe { *is_ordinary = false };
1193    true
1194}
1195
1196static PROXY_TRAPS: ProxyTraps = ProxyTraps {
1197    // TODO: These traps should change their behavior depending on
1198    //       `IsPlatformObjectSameOrigin(this.[[Window]])`
1199    enter: None,
1200    getOwnPropertyDescriptor: Some(get_own_property_descriptor),
1201    defineProperty: Some(define_property),
1202    ownPropertyKeys: None,
1203    delete_: None,
1204    enumerate: None,
1205    getPrototypeIfOrdinary: Some(get_prototype_if_ordinary),
1206    getPrototype: None, // TODO: return `null` if cross origin-domain
1207    setPrototype: None,
1208    setImmutablePrototype: None,
1209    preventExtensions: None,
1210    isExtensible: None,
1211    has: Some(has),
1212    get: Some(get),
1213    set: Some(set),
1214    call: None,
1215    construct: None,
1216    hasOwn: None,
1217    getOwnEnumerablePropertyKeys: None,
1218    nativeCall: None,
1219    objectClassIs: None,
1220    className: None,
1221    fun_toString: None,
1222    boxedValue_unbox: None,
1223    defaultValue: None,
1224    trace: Some(trace),
1225    finalize: Some(finalize),
1226    objectMoved: None,
1227    isCallable: None,
1228    isConstructor: None,
1229};
1230
1231/// Proxy handler for a WindowProxy.
1232/// Has ownership of the inner pointer and deallocates it when it is no longer needed.
1233pub(crate) struct WindowProxyHandler(*const libc::c_void);
1234
1235impl MallocSizeOf for WindowProxyHandler {
1236    fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
1237        // FIXME(#6907) this is a pointer to memory allocated by `new` in NewProxyHandler in rust-mozjs.
1238        0
1239    }
1240}
1241
1242// Safety: Send and Sync is guaranteed since the underlying pointer and all its associated methods in C++ are const.
1243#[expect(unsafe_code)]
1244unsafe impl Send for WindowProxyHandler {}
1245// Safety: Send and Sync is guaranteed since the underlying pointer and all its associated methods in C++ are const.
1246#[expect(unsafe_code)]
1247unsafe impl Sync for WindowProxyHandler {}
1248
1249#[expect(unsafe_code)]
1250impl WindowProxyHandler {
1251    fn new(traps: &ProxyTraps) -> Self {
1252        // Safety: Foreign function generated by bindgen. Pointer is freed in drop to prevent memory leak.
1253        let ptr = unsafe { CreateWrapperProxyHandler(traps) };
1254        assert!(!ptr.is_null());
1255        Self(ptr)
1256    }
1257
1258    /// Returns a single, shared WindowProxyHandler that contains XORIGIN_PROXY_TRAPS.
1259    pub(crate) fn x_origin_proxy_handler() -> &'static Self {
1260        use std::sync::OnceLock;
1261        /// We are sharing a single instance for the entire programs here due to lifetime issues.
1262        /// The pointer in self.0 is known to C++ and visited by the GC. Hence, we don't know when
1263        /// it is safe to free it.
1264        /// Sharing a single instance should be fine because all methods on this pointer in C++
1265        /// are const and don't modify its internal state.
1266        static SINGLETON: OnceLock<WindowProxyHandler> = OnceLock::new();
1267        SINGLETON.get_or_init(|| Self::new(&XORIGIN_PROXY_TRAPS))
1268    }
1269
1270    /// Returns a single, shared WindowProxyHandler that contains normal PROXY_TRAPS.
1271    pub(crate) fn proxy_handler() -> &'static Self {
1272        use std::sync::OnceLock;
1273        /// We are sharing a single instance for the entire programs here due to lifetime issues.
1274        /// The pointer in self.0 is known to C++ and visited by the GC. Hence, we don't know when
1275        /// it is safe to free it.
1276        /// Sharing a single instance should be fine because all methods on this pointer in C++
1277        /// are const and don't modify its internal state.
1278        static SINGLETON: OnceLock<WindowProxyHandler> = OnceLock::new();
1279        SINGLETON.get_or_init(|| Self::new(&PROXY_TRAPS))
1280    }
1281
1282    /// Creates a new WindowProxy object on the C++ side and returns the pointer to it.
1283    /// The pointer should be owned by the GC.
1284    fn new_window_proxy(
1285        &self,
1286        cx: &mut JSContext,
1287        window_jsobject: js::gc::HandleObject,
1288    ) -> *mut JSObject {
1289        let obj = unsafe { NewWindowProxy(cx, window_jsobject, self.0) };
1290        assert!(!obj.is_null());
1291        obj
1292    }
1293}
1294
1295#[expect(unsafe_code)]
1296impl Drop for WindowProxyHandler {
1297    fn drop(&mut self) {
1298        // Safety: Pointer is allocated by corresponding C++ function, owned by this
1299        // struct and not accessible from outside.
1300        unsafe {
1301            DeleteWrapperProxyHandler(self.0);
1302        }
1303    }
1304}
1305
1306// The proxy traps for cross-origin windows.
1307// These traps often throw security errors, and only pass on calls to methods
1308// defined in the DissimilarOriginWindow IDL.
1309
1310// TODO: reuse the infrastructure in `proxyhandler.rs`. For starters, the calls
1311//       to this function should be replaced with those to
1312//       `report_cross_origin_denial`.
1313#[expect(unsafe_code)]
1314fn throw_security_error(realm: &mut CurrentRealm) -> bool {
1315    if !unsafe { JS_IsExceptionPending(realm) } {
1316        let global = GlobalScope::from_current_realm(realm);
1317        throw_dom_exception(realm, &global, Error::Security(None));
1318    }
1319    false
1320}
1321
1322#[expect(unsafe_code)]
1323unsafe extern "C" fn has_xorigin(
1324    cx: *mut RawJSContext,
1325    proxy: RawHandleObject,
1326    id: RawHandleId,
1327    bp: *mut bool,
1328) -> bool {
1329    let mut cx = unsafe {
1330        // SAFETY: We are in SM hook
1331        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1332    };
1333    let mut slot = UndefinedValue();
1334    unsafe { GetProxyPrivate(*proxy.ptr, &mut slot) };
1335    rooted!(&in(cx) let target = slot.to_object());
1336    let mut found = false;
1337    unsafe { JS_HasOwnPropertyById(&mut cx, target.handle(), Handle::from_raw(id), &mut found) };
1338    if found {
1339        unsafe { *bp = true };
1340        true
1341    } else {
1342        let mut realm = CurrentRealm::assert(&mut cx);
1343        throw_security_error(&mut realm)
1344    }
1345}
1346
1347#[expect(unsafe_code)]
1348unsafe extern "C" fn get_xorigin(
1349    cx: *mut RawJSContext,
1350    proxy: RawHandleObject,
1351    receiver: RawHandleValue,
1352    id: RawHandleId,
1353    vp: RawMutableHandleValue,
1354) -> bool {
1355    let mut found = false;
1356    unsafe { has_xorigin(cx, proxy, id, &mut found) };
1357    found && unsafe { get(cx, proxy, receiver, id, vp) }
1358}
1359
1360#[expect(unsafe_code)]
1361unsafe extern "C" fn set_xorigin(
1362    cx: *mut RawJSContext,
1363    _: RawHandleObject,
1364    _: RawHandleId,
1365    _: RawHandleValue,
1366    _: RawHandleValue,
1367    _: *mut ObjectOpResult,
1368) -> bool {
1369    let mut cx = unsafe {
1370        // SAFETY: We are in SM hook
1371        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1372    };
1373    let mut realm = CurrentRealm::assert(&mut cx);
1374    throw_security_error(&mut realm)
1375}
1376
1377#[expect(unsafe_code)]
1378unsafe extern "C" fn delete_xorigin(
1379    cx: *mut RawJSContext,
1380    _: RawHandleObject,
1381    _: RawHandleId,
1382    _: *mut ObjectOpResult,
1383) -> bool {
1384    let mut cx = unsafe {
1385        // SAFETY: We are in SM hook
1386        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1387    };
1388    let mut realm = CurrentRealm::assert(&mut cx);
1389    throw_security_error(&mut realm)
1390}
1391
1392#[expect(unsafe_code)]
1393#[expect(non_snake_case)]
1394unsafe extern "C" fn getOwnPropertyDescriptor_xorigin(
1395    cx: *mut RawJSContext,
1396    proxy: RawHandleObject,
1397    id: RawHandleId,
1398    desc: RawMutableHandle<PropertyDescriptor>,
1399    is_none: *mut bool,
1400) -> bool {
1401    let mut found = false;
1402    unsafe { has_xorigin(cx, proxy, id, &mut found) };
1403    found && unsafe { get_own_property_descriptor(cx, proxy, id, desc, is_none) }
1404}
1405
1406#[expect(unsafe_code)]
1407#[expect(non_snake_case)]
1408unsafe extern "C" fn defineProperty_xorigin(
1409    cx: *mut RawJSContext,
1410    _: RawHandleObject,
1411    _: RawHandleId,
1412    _: RawHandle<PropertyDescriptor>,
1413    _: *mut ObjectOpResult,
1414) -> bool {
1415    let mut cx = unsafe {
1416        // SAFETY: We are in SM hook
1417        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1418    };
1419    let mut realm = CurrentRealm::assert(&mut cx);
1420    throw_security_error(&mut realm)
1421}
1422
1423#[expect(unsafe_code)]
1424#[expect(non_snake_case)]
1425unsafe extern "C" fn preventExtensions_xorigin(
1426    cx: *mut RawJSContext,
1427    _: RawHandleObject,
1428    _: *mut ObjectOpResult,
1429) -> bool {
1430    let mut cx = unsafe {
1431        // SAFETY: We are in SM hook
1432        JSContext::from_ptr(NonNull::new(cx).expect("JSContext should not be null in SM hook"))
1433    };
1434    let mut realm = CurrentRealm::assert(&mut cx);
1435    throw_security_error(&mut realm)
1436}
1437
1438static XORIGIN_PROXY_TRAPS: ProxyTraps = ProxyTraps {
1439    enter: None,
1440    getOwnPropertyDescriptor: Some(getOwnPropertyDescriptor_xorigin),
1441    defineProperty: Some(defineProperty_xorigin),
1442    ownPropertyKeys: None,
1443    delete_: Some(delete_xorigin),
1444    enumerate: None,
1445    getPrototypeIfOrdinary: None,
1446    getPrototype: None,
1447    setPrototype: None,
1448    setImmutablePrototype: None,
1449    preventExtensions: Some(preventExtensions_xorigin),
1450    isExtensible: None,
1451    has: Some(has_xorigin),
1452    get: Some(get_xorigin),
1453    set: Some(set_xorigin),
1454    call: None,
1455    construct: None,
1456    hasOwn: Some(has_xorigin),
1457    getOwnEnumerablePropertyKeys: None,
1458    nativeCall: None,
1459    objectClassIs: None,
1460    className: None,
1461    fun_toString: None,
1462    boxedValue_unbox: None,
1463    defaultValue: None,
1464    trace: Some(trace),
1465    finalize: Some(finalize),
1466    objectMoved: None,
1467    isCallable: None,
1468    isConstructor: None,
1469};
1470
1471// How WindowProxy objects are garbage collected.
1472
1473#[expect(unsafe_code)]
1474unsafe extern "C" fn finalize(_fop: *mut GCContext, obj: *mut JSObject) {
1475    let mut slot = UndefinedValue();
1476    unsafe { GetProxyReservedSlot(obj, 0, &mut slot) };
1477    let this = slot.to_private() as *mut WindowProxy;
1478    if this.is_null() {
1479        // GC during obj creation or after transplanting.
1480        return;
1481    }
1482    unsafe {
1483        (*this).reflector.drop_memory(&*this);
1484        let jsobject = (*this).reflector.get_jsobject().get();
1485        debug!(
1486            "WindowProxy finalize: {:p}, with reflector {:p} from {:p}.",
1487            this, jsobject, obj
1488        );
1489        let _ = Box::from_raw(this);
1490    }
1491}
1492
1493#[expect(unsafe_code)]
1494unsafe extern "C" fn trace(trc: *mut JSTracer, obj: *mut JSObject) {
1495    let mut slot = UndefinedValue();
1496    unsafe { GetProxyReservedSlot(obj, 0, &mut slot) };
1497    let this = slot.to_private() as *const WindowProxy;
1498    if this.is_null() {
1499        // GC during obj creation or after transplanting.
1500        return;
1501    }
1502    unsafe { (*this).trace(trc) };
1503}