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