Skip to main content

script/dom/geolocation/
geolocation.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/. */
4use std::cell::{Cell, RefCell};
5use std::rc::Rc;
6
7use dom_struct::dom_struct;
8use js::context::JSContext;
9use js::gc::HandleValue;
10use js::jsapi::IsCallable;
11use rustc_hash::FxHashSet;
12use script_bindings::callback::ExceptionHandling;
13use script_bindings::codegen::GenericBindings::GeolocationBinding::Geolocation_Binding::GeolocationMethods;
14use script_bindings::codegen::GenericBindings::GeolocationBinding::{
15    PositionCallback, PositionErrorCallback, PositionOptions,
16};
17use script_bindings::codegen::GenericBindings::PermissionStatusBinding::PermissionName;
18use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
19use script_bindings::domstring::DOMString;
20use script_bindings::error::{Error, Fallible};
21use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};
22use script_bindings::root::DomRoot;
23
24use crate::dom::bindings::codegen::DomTypeHolder::DomTypeHolder;
25use crate::dom::bindings::import::base::SafeJSContext;
26use crate::dom::bindings::reflector::DomGlobal;
27use crate::dom::geolocationpositionerror::GeolocationPositionError;
28use crate::dom::globalscope::GlobalScope;
29
30fn cast_error_callback(
31    cx: SafeJSContext,
32    error_callback: HandleValue,
33) -> Fallible<Option<Rc<PositionErrorCallback<DomTypeHolder>>>> {
34    if error_callback.get().is_object() {
35        let error_callback = error_callback.to_object();
36        #[expect(unsafe_code)]
37        unsafe {
38            if IsCallable(error_callback) {
39                Ok(Some(PositionErrorCallback::new(
40                    SafeJSContext::from_ptr(cx.raw_cx()),
41                    error_callback,
42                )))
43            } else {
44                Err(Error::Type(c"Value is not callable.".to_owned()))
45            }
46        }
47    } else if error_callback.get().is_null_or_undefined() {
48        Ok(None)
49    } else {
50        Err(Error::Type(c"Value is not an object.".to_owned()))
51    }
52}
53
54#[dom_struct]
55pub struct Geolocation {
56    reflector_: Reflector,
57    /// <https://www.w3.org/TR/geolocation/#dfn-watchids>
58    watch_ids: RefCell<FxHashSet<u32>>,
59    next_watch_id: Cell<u32>,
60}
61
62impl Geolocation {
63    fn new_inherited() -> Self {
64        Geolocation {
65            reflector_: Reflector::new(),
66            watch_ids: RefCell::new(FxHashSet::default()),
67            next_watch_id: Cell::new(1),
68        }
69    }
70
71    pub(crate) fn new(cx: &mut JSContext, global: &GlobalScope) -> DomRoot<Self> {
72        reflect_dom_object_with_cx(Box::new(Self::new_inherited()), global, cx)
73    }
74
75    /// <https://www.w3.org/TR/geolocation/#dfn-request-a-position>
76    fn request_position(
77        &self,
78        cx: &mut JSContext,
79        _success_callback: Rc<PositionCallback<DomTypeHolder>>,
80        error_callback: Option<Rc<PositionErrorCallback<DomTypeHolder>>>,
81        _options: &PositionOptions,
82        watch_id: Option<u32>,
83    ) -> Fallible<()> {
84        // Step 1. Let watchIDs be geolocation's [[watchIDs]].
85        // Step 2. Let document be the geolocation's relevant global object's associated Document.
86        let document = self.global().as_window().Document();
87        // Step 3. If document is not allowed to use the "geolocation" feature:
88        if !document.allowed_to_use_feature(PermissionName::Geolocation) {
89            if let Some(id) = watch_id {
90                // Step 3.1 If watchId was passed, remove watchId from watchIDs.
91                self.watch_ids.borrow_mut().remove(&id);
92            }
93            // Step 3.2. Call back with error passing errorCallback and PERMISSION_DENIED.
94            if let Some(error_callback) = error_callback {
95                let position_error = GeolocationPositionError::permission_denied(
96                    cx,
97                    &self.global(),
98                    DOMString::from("User denied Geolocation".to_string()),
99                );
100                error_callback.Call_(cx, self, &position_error, ExceptionHandling::Report)?;
101            }
102            // Step 3.3 Terminate this algorithm.
103            return Ok(());
104        }
105        // Step 4. If geolocation's environment settings object is a non-secure context:
106        if !self.global().is_secure_context() {
107            if let Some(id) = watch_id {
108                // Step 4.1 If watchId was passed, remove watchId from watchIDs.
109                self.watch_ids.borrow_mut().remove(&id);
110            }
111            // Step 4.2. Call back with error passing errorCallback and PERMISSION_DENIED.
112            if let Some(error_callback) = error_callback {
113                let position_error = GeolocationPositionError::permission_denied(
114                    cx,
115                    &self.global(),
116                    DOMString::from("Insecure context for Geolocation".to_string()),
117                );
118                error_callback.Call_(cx, self, &position_error, ExceptionHandling::Report)?;
119            }
120            // Step 4.3 Terminate this algorithm.
121            return Ok(());
122        }
123        // TODO: Step 5
124        // TODO: Step 6. Let descriptor be a new PermissionDescriptor whose name is "geolocation".
125
126        Ok(())
127    }
128}
129
130impl GeolocationMethods<DomTypeHolder> for Geolocation {
131    /// <https://www.w3.org/TR/geolocation/#dom-geolocation-getcurrentposition>
132    fn GetCurrentPosition(
133        &self,
134        cx: &mut JSContext,
135        success_callback: Rc<PositionCallback<DomTypeHolder>>,
136        error_callback: HandleValue,
137        options: &PositionOptions,
138    ) -> Fallible<()> {
139        let error_callback = cast_error_callback(cx.into(), error_callback)?;
140        // Step 1. If this's relevant global object's associated Document is not fully active:
141        if !self.global().as_window().Document().is_active() {
142            // Step 1.1 Call back with error errorCallback and POSITION_UNAVAILABLE.
143            if let Some(error_callback) = error_callback {
144                let position_error = GeolocationPositionError::position_unavailable(
145                    cx,
146                    &self.global(),
147                    DOMString::from("Document is not fully active".to_string()),
148                );
149                error_callback.Call_(cx, self, &position_error, ExceptionHandling::Report)?;
150            }
151            // Step 1.2 Terminate this algorithm.
152            return Ok(());
153        }
154        // Step 2. Request a position passing this, successCallback, errorCallback, and options.
155        self.request_position(cx, success_callback, error_callback, options, None)
156    }
157
158    /// <https://www.w3.org/TR/geolocation/#watchposition-method>
159    fn WatchPosition(
160        &self,
161        cx: &mut JSContext,
162        success_callback: Rc<PositionCallback<DomTypeHolder>>,
163        error_callback: HandleValue,
164        options: &PositionOptions,
165    ) -> Fallible<i32> {
166        let error_callback = cast_error_callback(cx.into(), error_callback)?;
167        // Step 1. If this's relevant global object's associated Document is not fully active:
168        if !self.global().as_window().Document().is_active() {
169            // Step 1.1 Call back with error errorCallback and POSITION_UNAVAILABLE.
170            if let Some(error_callback) = error_callback {
171                let position_error = GeolocationPositionError::position_unavailable(
172                    cx,
173                    &self.global(),
174                    DOMString::from("Document is not fully active".to_string()),
175                );
176                error_callback.Call_(cx, self, &position_error, ExceptionHandling::Report)?;
177            }
178            // Step 1.2 Return 0.
179            return Ok(0);
180        }
181        // Step 2. Let watchId be an implementation-defined unsigned long that is greater than zero.
182        let watch_id = self.next_watch_id.get();
183        self.next_watch_id.set(watch_id + 1);
184        // Step 3. Append watchId to this's [[watchIDs]].
185        self.watch_ids.borrow_mut().insert(watch_id);
186        // Step 4. Request a position passing this, successCallback, errorCallback, options, and watchId.
187        self.request_position(
188            cx,
189            success_callback,
190            error_callback,
191            options,
192            Some(watch_id),
193        )?;
194        // Step 5. Return watchId.
195        Ok(watch_id as i32)
196    }
197
198    /// <https://www.w3.org/TR/geolocation/#clearwatch-method>
199    fn ClearWatch(&self, watch_id: i32) {
200        let watch_id = u32::try_from(watch_id).ok();
201        if let Some(id) = watch_id {
202            self.watch_ids.borrow_mut().remove(&id);
203        }
204    }
205}