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