1use std::cell::Cell;
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7use dom_struct::dom_struct;
8use embedder_traits::{
9 AllowOrDeny, EmbedderMsg, GeolocationError, GeolocationRequestOptions, GeolocationResult,
10 GeolocationWatchId,
11};
12use js::context::JSContext;
13use rustc_hash::{FxHashMap, FxHashSet};
14use script_bindings::callback::{ExceptionHandling, OwnerWindow, RootedCallback, TracedCallback};
15use script_bindings::cell::DomRefCell;
16use script_bindings::codegen::GenericBindings::DocumentBinding::{
17 DocumentMethods, DocumentVisibilityState,
18};
19use script_bindings::codegen::GenericBindings::GeolocationBinding::Geolocation_Binding::GeolocationMethods;
20use script_bindings::codegen::GenericBindings::GeolocationBinding::{
21 PositionCallback, PositionErrorCallback, PositionOptions,
22};
23use script_bindings::codegen::GenericBindings::PermissionStatusBinding::{
24 PermissionName, PermissionState,
25};
26use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
27use script_bindings::domstring::DOMString;
28use script_bindings::reflector::{Reflector, reflect_dom_object};
29use script_bindings::root::DomRoot;
30use servo_base::generic_channel::GenericCallback;
31
32use crate::dom::bindings::codegen::DomTypeHolder::DomTypeHolder;
33use crate::dom::bindings::refcounted::Trusted;
34use crate::dom::bindings::reflector::DomGlobal;
35use crate::dom::bindings::root::MutNullableDom;
36use crate::dom::geolocationposition::GeolocationPosition;
37use crate::dom::geolocationpositionerror::GeolocationPositionError;
38use crate::dom::globalscope::GlobalScope;
39use crate::dom::permissions::{
40 AsyncPermissionRequest, descriptor_permission_state, record_permission_request_result,
41 request_permission_to_use_async,
42};
43use crate::event_loop::timers::{OneshotTimerCallback, OneshotTimerHandle};
44
45type RequestId = u32;
47
48#[derive(Clone, JSTraceable, MallocSizeOf)]
50struct RequestOptions {
51 enable_high_accuracy: bool,
53 timeout: u32,
55 maximum_age: u32,
57}
58
59impl From<&PositionOptions> for RequestOptions {
60 fn from(options: &PositionOptions) -> Self {
61 Self {
62 enable_high_accuracy: options.enableHighAccuracy,
63 timeout: options.timeout,
64 maximum_age: options.maximumAge,
65 }
66 }
67}
68
69impl From<RequestOptions> for GeolocationRequestOptions {
70 fn from(val: RequestOptions) -> Self {
71 GeolocationRequestOptions {
72 high_accuracy: val.enable_high_accuracy,
73 }
74 }
75}
76
77#[derive(JSTraceable, MallocSizeOf)]
79#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
80struct PositionRequest {
81 success_callback: TracedCallback<PositionCallback<DomTypeHolder>>,
82 error_callback: Option<TracedCallback<PositionErrorCallback<DomTypeHolder>>>,
83 options: RequestOptions,
84 watch_id: Option<u32>,
86 timeout_handle: Cell<Option<OneshotTimerHandle>>,
88 settled: Cell<bool>,
91 watching: Cell<bool>,
93 terminated: Cell<bool>,
96}
97
98impl PositionRequest {
99 fn is_watch(&self) -> bool {
100 self.watch_id.is_some()
101 }
102}
103
104enum Completion {
106 Success(Trusted<GeolocationPosition>),
107 Failure(PositionError),
108}
109
110enum PositionError {
113 PermissionDenied(&'static str),
114 PositionUnavailable(&'static str),
115 Timeout,
116 Embedder(GeolocationError),
118}
119
120impl PositionError {
121 fn create(
122 &self,
123 cx: &mut JSContext,
124 global: &GlobalScope,
125 ) -> DomRoot<GeolocationPositionError> {
126 match self {
127 Self::PermissionDenied(message) => {
128 GeolocationPositionError::permission_denied(cx, global, DOMString::from(*message))
129 },
130 Self::PositionUnavailable(message) => GeolocationPositionError::position_unavailable(
131 cx,
132 global,
133 DOMString::from(*message),
134 ),
135 Self::Timeout => GeolocationPositionError::timeout(
136 cx,
137 global,
138 DOMString::from_static("Acquiring the position timed out"),
139 ),
140 Self::Embedder(error) => {
141 GeolocationPositionError::from_embedder_error(cx, global, error)
142 },
143 }
144 }
145}
146
147fn now_as_epoch_timestamp() -> u64 {
149 SystemTime::now()
150 .duration_since(UNIX_EPOCH)
151 .map(|duration| duration.as_millis() as u64)
152 .unwrap_or_default()
153}
154
155#[dom_struct]
156pub struct Geolocation {
157 reflector_: Reflector,
158 watch_ids: DomRefCell<FxHashSet<u32>>,
160 next_watch_id: Cell<u32>,
161 requests: DomRefCell<FxHashMap<RequestId, PositionRequest>>,
163 next_request_id: Cell<RequestId>,
164 deferred_until_visible: DomRefCell<Vec<RequestId>>,
167 cached_position: MutNullableDom<GeolocationPosition>,
169}
170
171impl Geolocation {
172 fn new_inherited() -> Self {
173 Geolocation {
174 reflector_: Reflector::new(),
175 watch_ids: Default::default(),
176 next_watch_id: Cell::new(1),
177 requests: Default::default(),
178 next_request_id: Cell::new(1),
179 deferred_until_visible: Default::default(),
180 cached_position: Default::default(),
181 }
182 }
183
184 pub(crate) fn new(cx: &mut JSContext, global: &GlobalScope) -> DomRoot<Self> {
185 reflect_dom_object(cx, Box::new(Self::new_inherited()), global)
186 }
187
188 fn register_request(
189 &self,
190 success_callback: RootedCallback<PositionCallback<DomTypeHolder>>,
191 error_callback: Option<RootedCallback<PositionErrorCallback<DomTypeHolder>>>,
192 options: &PositionOptions,
193 watch_id: Option<u32>,
194 ) -> RequestId {
195 let request_id = self.next_request_id.get();
196 self.next_request_id.set(request_id.wrapping_add(1));
197 self.requests.borrow_mut().insert(
198 request_id,
199 PositionRequest {
200 success_callback: success_callback.to_traced(),
201 error_callback: error_callback.map(|callback| callback.to_traced()),
202 options: options.into(),
203 watch_id,
204 timeout_handle: Cell::new(None),
205 settled: Cell::new(false),
206 watching: Cell::new(false),
207 terminated: Cell::new(false),
208 },
209 );
210 request_id
211 }
212
213 fn discard_request(&self, request_id: RequestId) {
215 let Some((timeout_handle, watch_id, watching)) =
216 self.requests.borrow().get(&request_id).map(|request| {
217 (
218 request.timeout_handle.take(),
219 request.watch_id,
220 request.watching.get(),
221 )
222 })
223 else {
224 return;
225 };
226 self.requests.borrow_mut().remove(&request_id);
227
228 if let Some(handle) = timeout_handle {
229 self.global().unschedule_callback(handle);
230 }
231 if let (Some(watch_id), true) = (watch_id, watching) {
232 self.stop_embedder_watch(watch_id);
233 }
234 }
235
236 fn disarm_timeout(&self, request_id: RequestId) {
237 let handle = self
238 .requests
239 .borrow()
240 .get(&request_id)
241 .and_then(|request| request.timeout_handle.take());
242 if let Some(handle) = handle {
243 self.global().unschedule_callback(handle);
244 }
245 }
246
247 fn embedder_watch_id(&self, watch_id: u32) -> Option<GeolocationWatchId> {
248 let global = self.global();
249 Some(GeolocationWatchId {
250 webview_id: global.webview_id()?,
251 pipeline_id: global.pipeline_id(),
252 index: watch_id,
253 })
254 }
255
256 fn stop_embedder_watch(&self, watch_id: u32) {
257 let Some(id) = self.embedder_watch_id(watch_id) else {
258 return;
259 };
260 self.global()
261 .send_to_embedder(EmbedderMsg::StopGeolocationWatch(id));
262 }
263
264 pub(crate) fn stop_all_watches(&self) {
267 self.deferred_until_visible.borrow_mut().clear();
268 self.watch_ids.borrow_mut().clear();
269
270 for request in self.requests.borrow().values() {
271 if let Some(handle) = request.timeout_handle.take() {
272 self.global().unschedule_callback(handle);
273 }
274 if let (Some(watch_id), true) = (request.watch_id, request.watching.get()) {
275 self.stop_embedder_watch(watch_id);
276 }
277 }
278 self.requests.borrow_mut().clear();
279 }
280
281 fn document_can_receive_positions(&self) -> bool {
287 let document = self.global().as_window().Document();
288 document.is_fully_active() && document.VisibilityState() == DocumentVisibilityState::Visible
289 }
290
291 fn queue_completion(&self, request_id: RequestId, completion: Completion) {
297 let this = Trusted::new(self);
298 self.global()
299 .task_manager()
300 .geolocation_task_source()
301 .queue(task!(geolocation_completion: move |cx| {
302 this.root().report_completion(cx, request_id, completion);
303 }));
304 }
305
306 fn report_completion(&self, cx: &mut JSContext, request_id: RequestId, completion: Completion) {
308 let Some(keep_listening) = self
309 .requests
310 .borrow()
311 .get(&request_id)
312 .map(|request| request.is_watch() && !request.terminated.get())
313 else {
314 return;
315 };
316
317 match completion {
318 Completion::Success(position) => {
319 rooted!(&in(cx) let callback = self
320 .requests
321 .borrow()
322 .get(&request_id)
323 .map(|request| request.success_callback.clone()));
324 if let Some(ref callback) = *callback {
325 let position = position.root();
326 let _ = callback.Call_(cx, self, &position, ExceptionHandling::Report);
327 }
328 },
329 Completion::Failure(error) => {
330 rooted!(&in(cx) let callback = self
332 .requests
333 .borrow()
334 .get(&request_id)
335 .and_then(|request| request.error_callback.clone()));
336 if let Some(ref callback) = *callback {
337 let position_error = error.create(cx, &self.global());
338 let _ = callback.Call_(cx, self, &position_error, ExceptionHandling::Report);
339 }
340 },
341 }
342
343 if !keep_listening {
344 self.requests.borrow_mut().remove(&request_id);
345 }
346 }
347
348 fn terminate_with_error(&self, request_id: RequestId, error: PositionError) {
350 let Some((watch_id, was_watching)) =
351 self.requests.borrow().get(&request_id).map(|request| {
352 request.settled.set(true);
353 request.terminated.set(true);
354 (request.watch_id, request.watching.replace(false))
355 })
356 else {
357 return;
358 };
359 self.disarm_timeout(request_id);
360 if let Some(watch_id) = watch_id {
361 self.watch_ids.borrow_mut().remove(&watch_id);
362 if was_watching {
363 self.stop_embedder_watch(watch_id);
364 }
365 }
366 self.queue_completion(request_id, Completion::Failure(error));
367 }
368
369 fn complete_acquisition(&self, request_id: RequestId, completion: Completion) {
372 self.queue_completion(request_id, completion);
373 if let Some(request) = self.requests.borrow().get(&request_id) &&
374 request.is_watch()
375 {
376 request.settled.set(false);
377 }
378 }
379
380 fn request_position(
382 &self,
383 success_callback: RootedCallback<PositionCallback<DomTypeHolder>>,
384 error_callback: Option<RootedCallback<PositionErrorCallback<DomTypeHolder>>>,
385 options: &PositionOptions,
386 watch_id: Option<u32>,
387 ) {
388 let request_id = self.register_request(success_callback, error_callback, options, watch_id);
389
390 let document = self.global().as_window().Document();
393
394 if !document.allowed_to_use_feature(PermissionName::Geolocation) {
396 self.terminate_with_error(
398 request_id,
399 PositionError::PermissionDenied("User denied Geolocation"),
400 );
401 return;
402 }
403
404 if !self.global().is_secure_context() {
406 self.terminate_with_error(
408 request_id,
409 PositionError::PermissionDenied("Insecure context for Geolocation"),
410 );
411 return;
412 }
413
414 if document.VisibilityState() == DocumentVisibilityState::Hidden {
417 self.deferred_until_visible.borrow_mut().push(request_id);
418 return;
419 }
420
421 self.obtain_permission_then_acquire(request_id);
423 }
424
425 fn obtain_permission_then_acquire(&self, request_id: RequestId) {
428 if !self.requests.borrow().contains_key(&request_id) {
429 return;
432 }
433
434 let global = self.global();
437 let this = Trusted::new(self);
438 let task_source = global
439 .task_manager()
440 .geolocation_task_source()
441 .to_sendable();
442 let callback = GenericCallback::new(move |response: Result<AllowOrDeny, _>| {
443 let this = this.clone();
444 task_source.queue(task!(geolocation_permission_response: move |_cx| {
445 let this = this.root();
446 let state = match response {
447 Ok(response) => record_permission_request_result(
448 PermissionName::Geolocation,
449 &this.global(),
450 response,
451 ),
452 Err(_) => PermissionState::Denied,
453 };
454 this.handle_permission_state(request_id, state);
455 }));
456 })
457 .expect("Could not create callback in script.");
458
459 match request_permission_to_use_async(PermissionName::Geolocation, &global, callback) {
460 AsyncPermissionRequest::Settled(state) => {
461 self.handle_permission_state(request_id, state)
462 },
463 AsyncPermissionRequest::Prompting => {},
464 }
465 }
466
467 fn handle_permission_state(&self, request_id: RequestId, state: PermissionState) {
468 if state == PermissionState::Granted {
469 self.acquire_position(request_id);
471 } else {
472 self.terminate_with_error(
475 request_id,
476 PositionError::PermissionDenied("User denied Geolocation"),
477 );
478 }
479 }
480
481 fn acquire_position(&self, request_id: RequestId) {
483 let Some((options, watch_id)) = self
484 .requests
485 .borrow()
486 .get(&request_id)
487 .map(|request| (request.options.clone(), request.watch_id))
488 else {
489 return;
490 };
491
492 if let Some(watch_id) = watch_id &&
494 !self.watch_ids.borrow().contains(&watch_id)
495 {
496 self.discard_request(request_id);
497 return;
498 }
499
500 if let Some(request) = self.requests.borrow().get(&request_id) {
501 request.settled.set(false);
502 }
503
504 if descriptor_permission_state(PermissionName::Geolocation, Some(&self.global())) ==
507 PermissionState::Denied
508 {
509 self.terminate_with_error(
510 request_id,
511 PositionError::PermissionDenied("User denied Geolocation"),
512 );
513 return;
514 }
515
516 let acquisition_time = now_as_epoch_timestamp();
518
519 if options.maximum_age > 0 &&
522 let Some(cached_position) = self.cached_position.get()
523 {
524 let cache_time = acquisition_time.saturating_sub(options.maximum_age as u64);
525 if cached_position.timestamp() > cache_time &&
526 cached_position.is_high_accuracy() == options.enable_high_accuracy
527 {
528 self.settle(request_id);
529 self.complete_acquisition(
530 request_id,
531 Completion::Success(Trusted::new(&*cached_position)),
532 );
533 return;
534 }
535 }
536
537 if options.timeout == 0 {
540 self.settle(request_id);
541 self.complete_acquisition(request_id, Completion::Failure(PositionError::Timeout));
542 return;
543 }
544
545 self.arm_timeout(request_id, options.timeout);
548
549 let Some(embedder_msg) = self.build_acquisition_message(request_id, watch_id, options)
551 else {
552 self.settle(request_id);
553 self.complete_acquisition(
554 request_id,
555 Completion::Failure(PositionError::PositionUnavailable(
556 "No geolocation backend is available.",
557 )),
558 );
559 return;
560 };
561 if watch_id.is_some() &&
562 let Some(request) = self.requests.borrow().get(&request_id)
563 {
564 request.watching.set(true);
565 }
566 self.global().send_to_embedder(embedder_msg);
567 }
568
569 fn build_acquisition_message(
571 &self,
572 request_id: RequestId,
573 watch_id: Option<u32>,
574 options: RequestOptions,
575 ) -> Option<EmbedderMsg> {
576 let global = self.global();
577 let webview_id = global.webview_id()?;
578 let this = Trusted::new(self);
579 let task_source = global
580 .task_manager()
581 .geolocation_task_source()
582 .to_sendable();
583 let callback = GenericCallback::new(move |result: Result<GeolocationResult, _>| {
584 let this = this.clone();
585 let result = result.unwrap_or_else(|error| {
586 warn!("Failed to receive a position from the embedder ({error:?}).");
587 Err(GeolocationError::position_unavailable(
588 "Failed to communicate with the geolocation backend.",
589 ))
590 });
591 task_source.queue(task!(geolocation_position_acquired: move |cx| {
592 this.root().handle_acquisition_result(cx, request_id, result);
593 }));
594 })
595 .expect("Could not create callback in script.");
596
597 Some(match watch_id {
598 Some(watch_id) => EmbedderMsg::StartGeolocationWatch(
599 self.embedder_watch_id(watch_id)?,
600 options.into(),
601 callback,
602 ),
603 None => EmbedderMsg::RequestGeolocationPosition(webview_id, options.into(), callback),
604 })
605 }
606
607 fn handle_acquisition_result(
610 &self,
611 cx: &mut JSContext,
612 request_id: RequestId,
613 result: GeolocationResult,
614 ) {
615 let Some((options, watch_id, is_watch)) =
616 self.requests.borrow().get(&request_id).map(|request| {
617 (
618 request.options.clone(),
619 request.watch_id,
620 request.is_watch(),
621 )
622 })
623 else {
624 return;
625 };
626
627 if let Some(watch_id) = watch_id &&
631 !self.watch_ids.borrow().contains(&watch_id)
632 {
633 let terminated = self
634 .requests
635 .borrow()
636 .get(&request_id)
637 .is_some_and(|request| request.terminated.get());
638 if !terminated {
639 self.discard_request(request_id);
640 }
641 return;
642 }
643
644 if is_watch && !self.document_can_receive_positions() {
647 return;
648 }
649
650 if !self.settle(request_id) {
652 return;
653 }
654
655 let completion = match result {
656 Ok(data) => {
657 match GeolocationPosition::from_position_data(
659 cx,
660 &self.global(),
661 &data,
662 now_as_epoch_timestamp(),
663 options.enable_high_accuracy,
664 ) {
665 Some(position) => {
666 self.cached_position.set(Some(&position));
668 Completion::Success(Trusted::new(&*position))
669 },
670 None => Completion::Failure(PositionError::PositionUnavailable(
671 "The geolocation backend reported an unrepresentable position.",
672 )),
673 }
674 },
675 Err(error) => Completion::Failure(PositionError::Embedder(error)),
676 };
677 self.complete_acquisition(request_id, completion);
678 }
679
680 fn settle(&self, request_id: RequestId) -> bool {
683 let settled = self
684 .requests
685 .borrow()
686 .get(&request_id)
687 .map(|request| request.settled.replace(true));
688 match settled {
689 Some(false) => {
690 self.disarm_timeout(request_id);
691 true
692 },
693 _ => false,
694 }
695 }
696
697 fn arm_timeout(&self, request_id: RequestId, timeout: u32) {
698 let callback = OneshotTimerCallback::GeolocationTimeout(GeolocationTimeoutCallback {
699 geolocation: Trusted::new(self),
700 request_id,
701 });
702 let handle = self
703 .global()
704 .schedule_callback(callback, Duration::from_millis(timeout as u64));
705 if let Some(request) = self.requests.borrow().get(&request_id) {
706 request.timeout_handle.set(Some(handle));
707 }
708 }
709
710 fn handle_timeout(&self, request_id: RequestId) {
711 if let Some(request) = self.requests.borrow().get(&request_id) {
713 request.timeout_handle.set(None);
714 }
715 if self.settle(request_id) {
716 self.complete_acquisition(request_id, Completion::Failure(PositionError::Timeout));
717 }
718 }
719
720 pub(crate) fn handle_visibility_change(&self, visibility_state: DocumentVisibilityState) {
725 if visibility_state != DocumentVisibilityState::Visible {
726 return;
727 }
728 let deferred = std::mem::take(&mut *self.deferred_until_visible.borrow_mut());
729 for request_id in deferred {
730 self.obtain_permission_then_acquire(request_id);
731 }
732 }
733}
734
735impl GeolocationMethods<DomTypeHolder> for Geolocation {
736 fn GetCurrentPosition(
738 &self,
739 success_callback: RootedCallback<PositionCallback<DomTypeHolder>>,
740 error_callback: Option<RootedCallback<PositionErrorCallback<DomTypeHolder>>>,
741 options: &PositionOptions,
742 ) {
743 if !self.global().as_window().Document().is_fully_active() {
745 let request_id = self.register_request(success_callback, error_callback, options, None);
747 self.queue_completion(
748 request_id,
749 Completion::Failure(PositionError::PositionUnavailable(
750 "Document is not fully active",
751 )),
752 );
753 return;
755 }
756 self.request_position(success_callback, error_callback, options, None)
758 }
759
760 fn WatchPosition(
762 &self,
763 success_callback: RootedCallback<PositionCallback<DomTypeHolder>>,
764 error_callback: Option<RootedCallback<PositionErrorCallback<DomTypeHolder>>>,
765 options: &PositionOptions,
766 ) -> i32 {
767 if !self.global().as_window().Document().is_fully_active() {
769 let request_id = self.register_request(success_callback, error_callback, options, None);
771 self.queue_completion(
772 request_id,
773 Completion::Failure(PositionError::PositionUnavailable(
774 "Document is not fully active",
775 )),
776 );
777 return 0;
779 }
780 let watch_id = self.next_watch_id.get();
782 self.next_watch_id.set(watch_id + 1);
783 self.watch_ids.borrow_mut().insert(watch_id);
785 self.request_position(success_callback, error_callback, options, Some(watch_id));
788 watch_id as i32
790 }
791
792 fn ClearWatch(&self, watch_id: i32) {
794 let Ok(watch_id) = u32::try_from(watch_id) else {
795 return;
796 };
797 self.watch_ids.borrow_mut().remove(&watch_id);
798
799 let request_id = self
800 .requests
801 .borrow()
802 .iter()
803 .find(|(_, request)| request.watch_id == Some(watch_id))
804 .map(|(request_id, _)| *request_id);
805 if let Some(request_id) = request_id {
806 self.discard_request(request_id);
807 }
808 }
809}
810
811impl OwnerWindow<crate::DomTypeHolder> for Geolocation {}
812
813#[derive(JSTraceable, MallocSizeOf)]
815pub(crate) struct GeolocationTimeoutCallback {
816 #[ignore_malloc_size_of = "Because it is non-owning"]
817 geolocation: Trusted<Geolocation>,
818 request_id: RequestId,
819}
820
821impl GeolocationTimeoutCallback {
822 pub(crate) fn invoke(self) {
823 self.geolocation.root().handle_timeout(self.request_id);
824 }
825}