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::gc::HandleValue;
9use js::jsapi::IsCallable;
10use rustc_hash::FxHashSet;
11use script_bindings::callback::ExceptionHandling;
12use script_bindings::codegen::GenericBindings::GeolocationBinding::Geolocation_Binding::GeolocationMethods;
13use script_bindings::codegen::GenericBindings::GeolocationBinding::{
14    PositionCallback, PositionErrorCallback, PositionOptions,
15};
16use script_bindings::codegen::GenericBindings::PermissionStatusBinding::PermissionName;
17use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
18use script_bindings::domstring::DOMString;
19use script_bindings::error::{Error, Fallible};
20use script_bindings::reflector::Reflector;
21use script_bindings::root::DomRoot;
22use script_bindings::script_runtime::CanGc;
23
24use crate::dom::bindings::codegen::DomTypeHolder::DomTypeHolder;
25use crate::dom::bindings::import::base::SafeJSContext;
26use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
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(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Self> {
72        reflect_dom_object(Box::new(Self::new_inherited()), global, can_gc)
73    }
74
75    /// <https://www.w3.org/TR/geolocation/#dfn-request-a-position>
76    fn request_position(
77        &self,
78        _success_callback: Rc<PositionCallback<DomTypeHolder>>,
79        error_callback: Option<Rc<PositionErrorCallback<DomTypeHolder>>>,
80        _options: &PositionOptions,
81        watch_id: Option<u32>,
82        can_gc: CanGc,
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                error_callback.Call_(
96                    self,
97                    &GeolocationPositionError::permission_denied(
98                        &self.global(),
99                        DOMString::from("User denied Geolocation".to_string()),
100                        can_gc,
101                    ),
102                    ExceptionHandling::Report,
103                    can_gc,
104                )?;
105            }
106            // Step 3.3 Terminate this algorithm.
107            return Ok(());
108        }
109        // Step 4. If geolocation's environment settings object is a non-secure context:
110        if !self.global().is_secure_context() {
111            if let Some(id) = watch_id {
112                // Step 4.1 If watchId was passed, remove watchId from watchIDs.
113                self.watch_ids.borrow_mut().remove(&id);
114            }
115            // Step 4.2. Call back with error passing errorCallback and PERMISSION_DENIED.
116            if let Some(error_callback) = error_callback {
117                error_callback.Call_(
118                    self,
119                    &GeolocationPositionError::permission_denied(
120                        &self.global(),
121                        DOMString::from("Insecure context for Geolocation".to_string()),
122                        can_gc,
123                    ),
124                    ExceptionHandling::Report,
125                    can_gc,
126                )?;
127            }
128            // Step 4.3 Terminate this algorithm.
129            return Ok(());
130        }
131        // TODO: Step 5
132        // TODO: Step 6. Let descriptor be a new PermissionDescriptor whose name is "geolocation".
133
134        Ok(())
135    }
136}
137
138impl GeolocationMethods<DomTypeHolder> for Geolocation {
139    /// <https://www.w3.org/TR/geolocation/#dom-geolocation-getcurrentposition>
140    fn GetCurrentPosition(
141        &self,
142        context: SafeJSContext,
143        success_callback: Rc<PositionCallback<DomTypeHolder>>,
144        error_callback: HandleValue,
145        options: &PositionOptions,
146        can_gc: CanGc,
147    ) -> Fallible<()> {
148        let error_callback = cast_error_callback(context, error_callback)?;
149        // Step 1. If this's relevant global object's associated Document is not fully active:
150        if !self.global().as_window().Document().is_active() {
151            // Step 1.1 Call back with error errorCallback and POSITION_UNAVAILABLE.
152            if let Some(error_callback) = error_callback {
153                error_callback.Call_(
154                    self,
155                    &GeolocationPositionError::position_unavailable(
156                        &self.global(),
157                        DOMString::from("Document is not fully active".to_string()),
158                        can_gc,
159                    ),
160                    ExceptionHandling::Report,
161                    can_gc,
162                )?;
163            }
164            // Step 1.2 Terminate this algorithm.
165            return Ok(());
166        }
167        // Step 2. Request a position passing this, successCallback, errorCallback, and options.
168        self.request_position(success_callback, error_callback, options, None, can_gc)
169    }
170
171    /// <https://www.w3.org/TR/geolocation/#watchposition-method>
172    fn WatchPosition(
173        &self,
174        context: SafeJSContext,
175        success_callback: Rc<PositionCallback<DomTypeHolder>>,
176        error_callback: HandleValue,
177        options: &PositionOptions,
178        can_gc: CanGc,
179    ) -> Fallible<i32> {
180        let error_callback = cast_error_callback(context, error_callback)?;
181        // Step 1. If this's relevant global object's associated Document is not fully active:
182        if !self.global().as_window().Document().is_active() {
183            // Step 1.1 Call back with error errorCallback and POSITION_UNAVAILABLE.
184            if let Some(error_callback) = error_callback {
185                error_callback.Call_(
186                    self,
187                    &GeolocationPositionError::position_unavailable(
188                        &self.global(),
189                        DOMString::from("Document is not fully active".to_string()),
190                        can_gc,
191                    ),
192                    ExceptionHandling::Report,
193                    can_gc,
194                )?;
195            }
196            // Step 1.2 Return 0.
197            return Ok(0);
198        }
199        // Step 2. Let watchId be an implementation-defined unsigned long that is greater than zero.
200        let watch_id = self.next_watch_id.get();
201        self.next_watch_id.set(watch_id + 1);
202        // Step 3. Append watchId to this's [[watchIDs]].
203        self.watch_ids.borrow_mut().insert(watch_id);
204        // Step 4. Request a position passing this, successCallback, errorCallback, options, and watchId.
205        self.request_position(
206            success_callback,
207            error_callback,
208            options,
209            Some(watch_id),
210            can_gc,
211        )?;
212        // Step 5. Return watchId.
213        Ok(watch_id as i32)
214    }
215
216    /// <https://www.w3.org/TR/geolocation/#clearwatch-method>
217    fn ClearWatch(&self, watch_id: i32) {
218        let watch_id = u32::try_from(watch_id).ok();
219        if let Some(id) = watch_id {
220            self.watch_ids.borrow_mut().remove(&id);
221        }
222    }
223}