Skip to main content

script/
window_named_properties.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 http://mozilla.org/MPL/2.0/. */
4
5use std::ptr;
6use std::sync::LazyLock;
7
8use js::glue::{AppendToIdVector, CreateProxyHandler, ProxyTraps};
9use js::jsapi::{
10    Handle, HandleId, HandleObject, JSCLASS_DELAY_METADATA_BUILDER, JSCLASS_IS_PROXY,
11    JSCLASS_RESERVED_SLOTS_MASK, JSCLASS_RESERVED_SLOTS_SHIFT, JSClass, JSClass_NON_NATIVE,
12    JSContext, JSErrNum, JSPROP_READONLY, MutableHandle, MutableHandleIdVector,
13    MutableHandleObject, ObjectOpResult, PropertyDescriptor, ProxyClassExtension, ProxyClassOps,
14    ProxyObjectOps, SymbolCode, UndefinedHandleValue,
15};
16use js::jsid::SymbolId;
17use js::jsval::UndefinedValue;
18use js::rust::wrappers2::{GetWellKnownSymbol, JS_SetImmutablePrototype, NewProxyObject};
19use js::rust::{
20    Handle as RustHandle, HandleObject as RustHandleObject, MutableHandle as RustMutableHandle,
21    MutableHandleObject as RustMutableHandleObject,
22};
23use script_bindings::proxyhandler::set_property_descriptor;
24use script_bindings::str::DOMString;
25
26use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
27use crate::dom::bindings::root::Root;
28use crate::dom::bindings::utils::has_property_on_prototype;
29use crate::dom::globalscope::GlobalScope;
30use crate::dom::window::Window;
31use crate::js::conversions::ToJSValConvertible;
32
33struct SyncWrapper(*const libc::c_void);
34#[expect(unsafe_code)]
35unsafe impl Sync for SyncWrapper {}
36#[expect(unsafe_code)]
37unsafe impl Send for SyncWrapper {}
38
39static HANDLER: LazyLock<SyncWrapper> = LazyLock::new(|| {
40    let traps = ProxyTraps {
41        enter: None,
42        getOwnPropertyDescriptor: Some(get_own_property_descriptor),
43        defineProperty: Some(define_property),
44        ownPropertyKeys: Some(own_property_keys),
45        delete_: Some(delete),
46        enumerate: None,
47        getPrototypeIfOrdinary: Some(get_prototype_if_ordinary),
48        getPrototype: None,
49        setPrototype: None,
50        setImmutablePrototype: None,
51        preventExtensions: Some(prevent_extensions),
52        isExtensible: Some(is_extensible),
53        has: None,
54        get: None,
55        set: None,
56        call: None,
57        construct: None,
58        hasOwn: None,
59        getOwnEnumerablePropertyKeys: None,
60        nativeCall: None,
61        objectClassIs: None,
62        className: Some(class_name),
63        fun_toString: None,
64        boxedValue_unbox: None,
65        defaultValue: None,
66        trace: None,
67        finalize: None,
68        objectMoved: None,
69        isCallable: None,
70        isConstructor: None,
71    };
72
73    #[expect(unsafe_code)]
74    unsafe {
75        SyncWrapper(CreateProxyHandler(&traps, ptr::null()))
76    }
77});
78
79#[expect(unsafe_code)]
80unsafe extern "C" fn get_own_property_descriptor(
81    cx: *mut JSContext,
82    proxy: HandleObject,
83    id: HandleId,
84    desc: MutableHandle<PropertyDescriptor>,
85    is_none: *mut bool,
86) -> bool {
87    // SAFETY: it is safe to construct a JSContext from an engine callback.
88    let mut cx = unsafe { js::context::JSContext::from_ptr(ptr::NonNull::new(cx).unwrap()) };
89
90    if id.is_symbol() {
91        if id.get().asBits_ ==
92            SymbolId(unsafe { GetWellKnownSymbol(&cx, SymbolCode::toStringTag) }).asBits_
93        {
94            rooted!(&in(cx) let mut rval = UndefinedValue());
95            "WindowProperties".to_jsval(&mut cx, rval.handle_mut());
96            set_property_descriptor(
97                unsafe { RustMutableHandle::from_raw(desc) },
98                rval.handle(),
99                JSPROP_READONLY.into(),
100                unsafe { &mut *is_none },
101            );
102        }
103        return true;
104    }
105
106    let mut found = false;
107    let lookup_succeeded = has_property_on_prototype(
108        &mut cx,
109        unsafe { RustHandle::from_raw(proxy) },
110        unsafe { RustHandle::from_raw(id) },
111        &mut found,
112    );
113    if !lookup_succeeded {
114        return false;
115    }
116    if found {
117        return true;
118    }
119
120    let s = if id.is_string() {
121        unsafe {
122            DOMString::from_raw_js_string(&cx, id.to_string())
123                .expect("Should always be able to convert because id is string")
124        }
125    } else if id.is_int() {
126        // If the property key is an integer index, convert it to a String too.
127        // For indexed access on the window object, which may shadow this, see
128        // the getOwnPropertyDescriptor trap in dom/windowproxy.rs.
129        DOMString::from(id.to_int().to_string())
130    } else if id.is_symbol() {
131        // Symbol properties were already handled above.
132        unreachable!()
133    } else {
134        unimplemented!()
135    };
136    if s.is_empty() {
137        return true;
138    }
139
140    let window = Root::downcast::<Window>(unsafe { GlobalScope::from_object(proxy.get()) })
141        .expect("global is not a window");
142    if let Some(obj) = window.NamedGetter(&mut cx, s) {
143        rooted!(&in(cx) let mut rval = UndefinedValue());
144        obj.to_jsval(&mut cx, rval.handle_mut());
145        set_property_descriptor(
146            unsafe { RustMutableHandle::from_raw(desc) },
147            rval.handle(),
148            0,
149            unsafe { &mut *is_none },
150        );
151    }
152    true
153}
154
155#[expect(unsafe_code)]
156unsafe extern "C" fn own_property_keys(
157    cx: *mut JSContext,
158    _proxy: HandleObject,
159    props: MutableHandleIdVector,
160) -> bool {
161    // TODO is this all we need to return? compare with gecko:
162    // https://searchfox.org/mozilla-central/rev/af78418c4b5f2c8721d1a06486cf4cf0b33e1e8d/dom/base/WindowNamedPropertiesHandler.cpp#175-232
163    // see also https://github.com/whatwg/html/issues/9068
164    unsafe {
165        rooted!(in(cx) let mut rooted = SymbolId(js::jsapi::GetWellKnownSymbol(cx, SymbolCode::toStringTag)));
166        AppendToIdVector(props, rooted.handle().into());
167    }
168    true
169}
170
171#[expect(unsafe_code)]
172unsafe extern "C" fn define_property(
173    _cx: *mut JSContext,
174    _proxy: HandleObject,
175    _id: HandleId,
176    _desc: Handle<PropertyDescriptor>,
177    result: *mut ObjectOpResult,
178) -> bool {
179    unsafe {
180        (*result).code_ = JSErrNum::JSMSG_CANT_DEFINE_WINDOW_NAMED_PROPERTY as usize;
181    }
182    true
183}
184
185#[expect(unsafe_code)]
186unsafe extern "C" fn delete(
187    _cx: *mut JSContext,
188    _proxy: HandleObject,
189    _id: HandleId,
190    result: *mut ObjectOpResult,
191) -> bool {
192    unsafe {
193        (*result).code_ = JSErrNum::JSMSG_CANT_DELETE_WINDOW_NAMED_PROPERTY as usize;
194    }
195    true
196}
197
198#[expect(unsafe_code)]
199unsafe extern "C" fn get_prototype_if_ordinary(
200    _cx: *mut JSContext,
201    proxy: HandleObject,
202    is_ordinary: *mut bool,
203    proto: MutableHandleObject,
204) -> bool {
205    unsafe {
206        *is_ordinary = true;
207        proto.set(js::jsapi::GetStaticPrototype(proxy.get()));
208    }
209    true
210}
211
212#[expect(unsafe_code)]
213unsafe extern "C" fn prevent_extensions(
214    _cx: *mut JSContext,
215    _proxy: HandleObject,
216    result: *mut ObjectOpResult,
217) -> bool {
218    unsafe {
219        (*result).code_ = JSErrNum::JSMSG_CANT_PREVENT_EXTENSIONS as usize;
220    }
221    true
222}
223
224#[expect(unsafe_code)]
225unsafe extern "C" fn is_extensible(
226    _cx: *mut JSContext,
227    _proxy: HandleObject,
228    extensible: *mut bool,
229) -> bool {
230    unsafe {
231        *extensible = true;
232    }
233    true
234}
235
236#[expect(unsafe_code)]
237unsafe extern "C" fn class_name(_cx: *mut JSContext, _proxy: HandleObject) -> *const libc::c_char {
238    c"WindowProperties".as_ptr()
239}
240
241// Maybe this should be a DOMJSClass. See https://bugzilla.mozilla.org/show_bug.cgi?id=787070
242#[expect(unsafe_code)]
243static CLASS: JSClass = JSClass {
244    name: c"WindowProperties".as_ptr(),
245    flags: JSClass_NON_NATIVE |
246        JSCLASS_IS_PROXY |
247        JSCLASS_DELAY_METADATA_BUILDER |
248        ((1 & JSCLASS_RESERVED_SLOTS_MASK) << JSCLASS_RESERVED_SLOTS_SHIFT), /* JSCLASS_HAS_RESERVED_SLOTS(1) */
249    cOps: unsafe { &ProxyClassOps },
250    spec: ptr::null(),
251    ext: unsafe { &ProxyClassExtension },
252    oOps: unsafe { &ProxyObjectOps },
253};
254
255#[expect(unsafe_code)]
256pub(crate) fn create(
257    cx: &mut js::context::JSContext,
258    proto: RustHandleObject,
259    mut properties_obj: RustMutableHandleObject,
260) {
261    unsafe {
262        properties_obj.set(NewProxyObject(
263            cx,
264            HANDLER.0,
265            RustHandle::from_raw(UndefinedHandleValue),
266            proto.get(),
267            &CLASS,
268            false,
269        ));
270    }
271    assert!(!properties_obj.get().is_null());
272    let mut succeeded = false;
273    unsafe {
274        assert!(JS_SetImmutablePrototype(
275            cx,
276            properties_obj.handle(),
277            &mut succeeded
278        ));
279    }
280    assert!(succeeded);
281}