1#[cfg(feature = "gamepad")]
6use std::cell::Cell;
7use std::convert::TryInto;
8use std::ops::Deref;
9use std::sync::LazyLock;
10
11use dom_struct::dom_struct;
12use embedder_traits::{EmbedderMsg, ProtocolHandlerUpdateRegistration, RegisterOrUnregister};
13use headers::HeaderMap;
14use http::header::{self, HeaderValue};
15#[cfg(feature = "webgpu")]
16use js::context::JSContext;
17use js::rust::MutableHandleValue;
18use net_traits::blob_url_store::UrlWithBlobClaim;
19use net_traits::request::{
20 CredentialsMode, Destination, RequestBuilder, RequestId, RequestMode,
21 is_cors_safelisted_request_content_type,
22};
23use net_traits::{FetchMetadata, NetworkError, ResourceFetchTiming};
24use regex::Regex;
25#[cfg(feature = "gamepad")]
26use script_bindings::cell::DomRefCell;
27use script_bindings::reflector::{Reflector, reflect_dom_object};
28use servo_base::generic_channel;
29use servo_config::pref;
30use servo_url::ServoUrl;
31
32use crate::body::Extractable;
33use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods;
34#[cfg(feature = "gamepad")]
35use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionName;
36use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
37use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit;
38use crate::dom::bindings::error::{Error, Fallible};
39use crate::dom::bindings::refcounted::Trusted;
40use crate::dom::bindings::reflector::DomGlobal;
41use crate::dom::bindings::root::{DomRoot, MutNullableDom};
42use crate::dom::bindings::str::{DOMString, USVString};
43use crate::dom::bindings::utils::to_frozen_array;
44#[cfg(feature = "bluetooth")]
45use crate::dom::bluetooth::Bluetooth;
46use crate::dom::clipboard::Clipboard;
47use crate::dom::credentialmanagement::credentialscontainer::CredentialsContainer;
48use crate::dom::csp::{GlobalCspReporting, Violation};
49#[cfg(feature = "gamepad")]
50use crate::dom::gamepad::Gamepad;
51use crate::dom::geolocation::Geolocation;
52use crate::dom::globalscope::GlobalScope;
53use crate::dom::mediadevices::MediaDevices;
54use crate::dom::mediasession::MediaSession;
55use crate::dom::mimetypearray::MimeTypeArray;
56use crate::dom::navigatorinfo;
57use crate::dom::performance::performanceresourcetiming::InitiatorType;
58use crate::dom::permissions::Permissions;
59use crate::dom::pluginarray::PluginArray;
60use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
61use crate::dom::servointernals::ServoInternals;
62use crate::dom::storagemanager::StorageManager;
63use crate::dom::types::UserActivation;
64use crate::dom::wakelock::WakeLock;
65#[cfg(feature = "webgpu")]
66use crate::dom::webgpu::gpu::GPU;
67use crate::dom::window::Window;
68#[cfg(feature = "webxr")]
69use crate::dom::xrsystem::XRSystem;
70use crate::fetch::RequestWithGlobalScope;
71use crate::network_listener::{FetchResponseListener, ResourceTimingListener, submit_timing};
72use crate::script_runtime::CanGc;
73
74pub(super) fn hardware_concurrency() -> u64 {
75 static CPUS: LazyLock<u64> = LazyLock::new(|| num_cpus::get().try_into().unwrap_or(1));
76
77 *CPUS
78}
79
80static SAFELISTED_SCHEMES: [&str; 24] = [
82 "bitcoin",
83 "ftp",
84 "ftps",
85 "geo",
86 "im",
87 "irc",
88 "ircs",
89 "magnet",
90 "mailto",
91 "matrix",
92 "mms",
93 "news",
94 "nntp",
95 "openpgp4fpr",
96 "sftp",
97 "sip",
98 "sms",
99 "smsto",
100 "ssh",
101 "tel",
102 "urn",
103 "webcal",
104 "wtai",
105 "xmpp",
106];
107
108fn matches_web_plus_protocol(scheme: &str) -> bool {
110 static WEB_PLUS_SCHEME_GRAMMAR: LazyLock<Regex> =
111 LazyLock::new(|| Regex::new(r#"^web\+[a-z]+$"#).unwrap());
112
113 WEB_PLUS_SCHEME_GRAMMAR.is_match(scheme)
114}
115
116#[dom_struct]
117pub(crate) struct Navigator {
118 reflector_: Reflector,
119 #[cfg(feature = "bluetooth")]
120 bluetooth: MutNullableDom<Bluetooth>,
121 credentials: MutNullableDom<CredentialsContainer>,
122 plugins: MutNullableDom<PluginArray>,
123 mime_types: MutNullableDom<MimeTypeArray>,
124 service_worker: MutNullableDom<ServiceWorkerContainer>,
125 #[cfg(feature = "webxr")]
126 xr: MutNullableDom<XRSystem>,
127 mediadevices: MutNullableDom<MediaDevices>,
128 #[cfg(feature = "gamepad")]
130 gamepads: DomRefCell<Vec<MutNullableDom<Gamepad>>>,
131 permissions: MutNullableDom<Permissions>,
132 mediasession: MutNullableDom<MediaSession>,
133 clipboard: MutNullableDom<Clipboard>,
134 storage: MutNullableDom<StorageManager>,
135 #[cfg(feature = "webgpu")]
136 gpu: MutNullableDom<GPU>,
137 #[cfg(feature = "gamepad")]
139 has_gamepad_gesture: Cell<bool>,
140 servo_internals: MutNullableDom<ServoInternals>,
141 user_activation: MutNullableDom<UserActivation>,
142 wake_lock: MutNullableDom<WakeLock>,
143}
144
145impl Navigator {
146 fn new_inherited() -> Navigator {
147 Navigator {
148 reflector_: Reflector::new(),
149 #[cfg(feature = "bluetooth")]
150 bluetooth: Default::default(),
151 credentials: Default::default(),
152 plugins: Default::default(),
153 mime_types: Default::default(),
154 service_worker: Default::default(),
155 #[cfg(feature = "webxr")]
156 xr: Default::default(),
157 mediadevices: Default::default(),
158 #[cfg(feature = "gamepad")]
159 gamepads: Default::default(),
160 permissions: Default::default(),
161 mediasession: Default::default(),
162 clipboard: Default::default(),
163 storage: Default::default(),
164 #[cfg(feature = "webgpu")]
165 gpu: Default::default(),
166 #[cfg(feature = "gamepad")]
167 has_gamepad_gesture: Cell::new(false),
168 servo_internals: Default::default(),
169 user_activation: Default::default(),
170 wake_lock: Default::default(),
171 }
172 }
173
174 pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<Navigator> {
175 reflect_dom_object(Box::new(Navigator::new_inherited()), window, can_gc)
176 }
177
178 #[cfg(feature = "webxr")]
179 pub(crate) fn xr(&self) -> Option<DomRoot<XRSystem>> {
180 self.xr.get()
181 }
182
183 #[cfg(feature = "gamepad")]
184 pub(crate) fn get_gamepad(&self, index: usize) -> Option<DomRoot<Gamepad>> {
185 self.gamepads.borrow().get(index).and_then(|g| g.get())
186 }
187
188 #[cfg(feature = "gamepad")]
189 pub(crate) fn set_gamepad(&self, index: usize, gamepad: Option<&Gamepad>) {
190 if let Some(gamepad_to_set) = self.gamepads.borrow().get(index) {
191 gamepad_to_set.set(gamepad);
192 }
193 }
194
195 #[cfg(feature = "gamepad")]
197 pub(crate) fn select_gamepad_index(&self) -> u32 {
198 let mut gamepad_list = self.gamepads.borrow_mut();
199 if let Some(index) = gamepad_list.iter().position(|g| g.get().is_none()) {
200 index as u32
201 } else {
202 let len = gamepad_list.len();
203 gamepad_list.resize_with(len + 1, Default::default);
204 len as u32
205 }
206 }
207
208 #[cfg(feature = "gamepad")]
210 pub(crate) fn shrink_gamepads_list(&self) {
211 let mut gamepad_list = self.gamepads.borrow_mut();
212 for i in (0..gamepad_list.len()).rev() {
213 if gamepad_list.get(i).is_none() {
214 gamepad_list.remove(i);
215 } else {
216 break;
217 }
218 }
219 }
220
221 #[cfg(feature = "gamepad")]
222 pub(crate) fn get_connected_gamepad(&self) -> Vec<DomRoot<Gamepad>> {
223 self.gamepads
224 .borrow()
225 .iter()
226 .filter_map(|gamepad| gamepad.get())
227 .filter(|gamepad| gamepad.connected())
228 .collect()
229 }
230
231 #[cfg(feature = "gamepad")]
232 pub(crate) fn has_gamepad_gesture(&self) -> bool {
233 self.has_gamepad_gesture.get()
234 }
235
236 #[cfg(feature = "gamepad")]
237 pub(crate) fn set_has_gamepad_gesture(&self, has_gamepad_gesture: bool) {
238 self.has_gamepad_gesture.set(has_gamepad_gesture);
239 }
240
241 fn normalize_protocol_handler_parameters(
243 &self,
244 scheme: DOMString,
245 url: USVString,
246 ) -> Fallible<(String, ServoUrl)> {
247 let scheme = scheme.to_ascii_lowercase();
249 if !SAFELISTED_SCHEMES.contains(&scheme.as_ref()) && !matches_web_plus_protocol(&scheme) {
252 return Err(Error::Security(None));
253 }
254 if !url.contains("%s") {
256 return Err(Error::Syntax(Some(
257 "Missing replacement string %s in URL".to_owned(),
258 )));
259 }
260 let environment = self.global();
262 let window = environment.as_window();
264 let Ok(url) = window.Document().encoding_parse_a_url(&url) else {
265 return Err(Error::Syntax(Some("Cannot parse URL".to_owned())));
267 };
268 if !matches!(url.scheme(), "http" | "https") {
271 return Err(Error::Security(None));
272 }
273 let environment_origin = environment.origin().immutable().clone();
274 if url.origin() != environment_origin {
275 return Err(Error::Security(None));
276 }
277 assert!(url.is_potentially_trustworthy());
279 Ok((scheme, url))
281 }
282
283 fn send_protocol_update_registration_to_embedder(
284 &self,
285 registration: ProtocolHandlerUpdateRegistration,
286 ) {
287 let global = self.global();
288 let window = global.as_window();
289 let (sender, _) = generic_channel::channel().unwrap();
290 let _ = global
291 .script_to_embedder_chan()
292 .send(EmbedderMsg::AllowProtocolHandlerRequest(
293 window.webview_id(),
294 registration,
295 sender,
296 ));
297 }
298}
299
300impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
301 fn Product(&self) -> DOMString {
303 navigatorinfo::Product()
304 }
305
306 fn ProductSub(&self) -> DOMString {
308 navigatorinfo::ProductSub()
309 }
310
311 fn Vendor(&self) -> DOMString {
313 navigatorinfo::Vendor()
314 }
315
316 fn VendorSub(&self) -> DOMString {
318 navigatorinfo::VendorSub()
319 }
320
321 fn TaintEnabled(&self) -> bool {
323 navigatorinfo::TaintEnabled()
324 }
325
326 fn AppName(&self) -> DOMString {
328 navigatorinfo::AppName()
329 }
330
331 fn AppCodeName(&self) -> DOMString {
333 navigatorinfo::AppCodeName()
334 }
335
336 fn Platform(&self) -> DOMString {
338 navigatorinfo::Platform()
339 }
340
341 fn UserAgent(&self) -> DOMString {
343 navigatorinfo::UserAgent(&pref!(user_agent))
344 }
345
346 fn AppVersion(&self) -> DOMString {
348 navigatorinfo::AppVersion()
349 }
350
351 #[cfg(feature = "bluetooth")]
353 fn Bluetooth(&self, cx: &mut js::context::JSContext) -> DomRoot<Bluetooth> {
354 self.bluetooth
355 .or_init(|| Bluetooth::new(cx, &self.global()))
356 }
357
358 fn Credentials(&self) -> DomRoot<CredentialsContainer> {
360 self.credentials
361 .or_init(|| CredentialsContainer::new(&self.global(), CanGc::deprecated_note()))
362 }
363
364 fn Geolocation(&self) -> DomRoot<Geolocation> {
366 Geolocation::new(&self.global(), CanGc::deprecated_note())
367 }
368
369 fn Language(&self) -> DOMString {
371 navigatorinfo::Language()
372 }
373
374 fn Languages(&self, cx: &mut js::context::JSContext, retval: MutableHandleValue) {
376 to_frozen_array(&[self.Language()], cx.into(), retval, CanGc::from_cx(cx))
377 }
378
379 fn OnLine(&self) -> bool {
381 true
382 }
383
384 fn Plugins(&self) -> DomRoot<PluginArray> {
386 self.plugins
387 .or_init(|| PluginArray::new(&self.global(), CanGc::deprecated_note()))
388 }
389
390 fn MimeTypes(&self) -> DomRoot<MimeTypeArray> {
392 self.mime_types
393 .or_init(|| MimeTypeArray::new(&self.global(), CanGc::deprecated_note()))
394 }
395
396 fn JavaEnabled(&self) -> bool {
398 false
399 }
400
401 fn PdfViewerEnabled(&self) -> bool {
403 false
404 }
405
406 fn ServiceWorker(&self, cx: &mut js::context::JSContext) -> DomRoot<ServiceWorkerContainer> {
408 self.service_worker
409 .or_init(|| ServiceWorkerContainer::new(cx, &self.global()))
410 }
411
412 fn CookieEnabled(&self) -> bool {
414 true
415 }
416
417 #[cfg(feature = "gamepad")]
419 fn GetGamepads(&self) -> Fallible<Vec<Option<DomRoot<Gamepad>>>> {
420 use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods;
421
422 let global = self.global();
424 let window = global.as_window();
425 let doc = window.Document();
426
427 if !doc.is_fully_active() {
429 return Ok(Vec::new());
430 }
431
432 if !doc.allowed_to_use_feature(PermissionName::Gamepad) {
435 return Err(Error::Security(Some(
436 "Gamepad permission not allowed".into(),
437 )));
438 }
439
440 if !self.has_gamepad_gesture.get() {
442 return Ok(Vec::new());
443 }
444
445 let now = *window.Performance().Now();
447
448 Ok(self
451 .gamepads
452 .borrow()
453 .iter()
454 .map(|slot| {
455 slot.get().inspect(|gamepad| {
456 if !gamepad.exposed() {
458 gamepad.set_exposed(true);
460 gamepad.update_timestamp(now);
462 }
463 })
464 })
465 .collect()) }
468 fn Permissions(&self) -> DomRoot<Permissions> {
470 self.permissions
471 .or_init(|| Permissions::new(&self.global(), CanGc::deprecated_note()))
472 }
473
474 #[cfg(feature = "webxr")]
476 fn Xr(&self) -> DomRoot<XRSystem> {
477 self.xr
478 .or_init(|| XRSystem::new(self.global().as_window(), CanGc::deprecated_note()))
479 }
480
481 fn MediaDevices(&self) -> DomRoot<MediaDevices> {
483 self.mediadevices
484 .or_init(|| MediaDevices::new(&self.global(), CanGc::deprecated_note()))
485 }
486
487 fn MediaSession(&self) -> DomRoot<MediaSession> {
489 self.mediasession.or_init(|| {
490 let global = self.global();
498 let window = global.as_window();
499 MediaSession::new(window, CanGc::deprecated_note())
500 })
501 }
502
503 #[cfg(feature = "webgpu")]
505 fn Gpu(&self, cx: &mut JSContext) -> DomRoot<GPU> {
506 self.gpu.or_init(|| GPU::new(cx, &self.global()))
507 }
508
509 fn HardwareConcurrency(&self) -> u64 {
511 hardware_concurrency()
512 }
513
514 fn Clipboard(&self, cx: &mut js::context::JSContext) -> DomRoot<Clipboard> {
516 self.clipboard
517 .or_init(|| Clipboard::new(cx, &self.global()))
518 }
519
520 fn Storage(&self, cx: &mut js::context::JSContext) -> DomRoot<StorageManager> {
522 self.storage
523 .or_init(|| StorageManager::new(&self.global(), CanGc::from_cx(cx)))
524 }
525
526 fn SendBeacon(
528 &self,
529 cx: &mut js::context::JSContext,
530 url: USVString,
531 data: Option<BodyInit>,
532 ) -> Fallible<bool> {
533 let global = self.global();
534 let base = global.api_base_url();
536 let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
544 return Err(Error::Type(c"Cannot parse URL".to_owned()));
545 };
546 if !matches!(url.scheme(), "http" | "https") {
547 return Err(Error::Type(c"URL is not http(s)".to_owned()));
548 }
549 let mut request_body = None;
550 let mut headers = HeaderMap::with_capacity(1);
552 let mut cors_mode = RequestMode::NoCors;
554 if let Some(data) = data {
556 let extracted_body = data.extract(cx, &global, true)?;
559 if let Some(total_bytes) = extracted_body.total_bytes {
563 let in_flight_keep_alive_bytes =
564 global.total_size_of_in_flight_keep_alive_records();
565 if total_bytes as u64 + in_flight_keep_alive_bytes > 64 * 1024 {
566 return Ok(false);
567 }
568 }
569 if let Some(content_type) = extracted_body.content_type.as_ref() {
571 cors_mode = RequestMode::CorsMode;
573 if is_cors_safelisted_request_content_type(content_type.as_bytes().deref()) {
576 cors_mode = RequestMode::NoCors;
577 }
578 headers.insert(
583 header::CONTENT_TYPE,
584 HeaderValue::from_str(&content_type.str()).unwrap(),
585 );
586 }
587 request_body = Some(extracted_body.into_net_request_body().0);
588 }
589 let request = RequestBuilder::new(
591 None,
592 UrlWithBlobClaim::from_url_without_having_claimed_blob(url.clone()),
593 global.get_referrer(),
594 )
595 .mode(cors_mode)
596 .destination(Destination::None)
597 .with_global_scope(&global)
598 .method(http::Method::POST)
599 .body(request_body)
600 .keep_alive(true)
601 .credentials_mode(CredentialsMode::Include)
602 .headers(headers);
603 global.fetch(
605 request,
606 BeaconFetchListener {
607 url,
608 global: Trusted::new(&global),
609 },
610 global.task_manager().networking_task_source().into(),
611 );
612 Ok(true)
615 }
616
617 fn Servo(&self) -> DomRoot<ServoInternals> {
619 self.servo_internals
620 .or_init(|| ServoInternals::new(&self.global(), CanGc::deprecated_note()))
621 }
622
623 fn RegisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
625 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
628 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
640 scheme,
641 url,
642 register_or_unregister: RegisterOrUnregister::Register,
643 });
644 Ok(())
645 }
646
647 fn UnregisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
649 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
652 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
654 scheme,
655 url,
656 register_or_unregister: RegisterOrUnregister::Unregister,
657 });
658 Ok(())
659 }
660
661 fn UserActivation(&self, cx: &mut js::context::JSContext) -> DomRoot<UserActivation> {
663 self.user_activation
664 .or_init(|| UserActivation::new(cx, &self.global()))
665 }
666
667 fn WakeLock(&self, cx: &mut js::context::JSContext) -> DomRoot<WakeLock> {
669 self.wake_lock.or_init(|| WakeLock::new(cx, &self.global()))
670 }
671}
672
673struct BeaconFetchListener {
674 url: ServoUrl,
676 global: Trusted<GlobalScope>,
678}
679
680impl FetchResponseListener for BeaconFetchListener {
681 fn process_request_body(&mut self, _: RequestId) {}
682
683 fn process_response(
684 &mut self,
685 _: &mut js::context::JSContext,
686 _: RequestId,
687 fetch_metadata: Result<FetchMetadata, NetworkError>,
688 ) {
689 _ = fetch_metadata;
690 }
691
692 fn process_response_chunk(
693 &mut self,
694 _: &mut js::context::JSContext,
695 _: RequestId,
696 chunk: Vec<u8>,
697 ) {
698 _ = chunk;
699 }
700
701 fn process_response_eof(
702 self,
703 cx: &mut js::context::JSContext,
704 _: RequestId,
705 response: Result<(), NetworkError>,
706 timing: ResourceFetchTiming,
707 ) {
708 submit_timing(cx, &self, &response, &timing);
709 }
710
711 fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
712 let global = self.resource_timing_global();
713 global.report_csp_violations(violations, None, None);
714 }
715}
716
717impl ResourceTimingListener for BeaconFetchListener {
718 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
719 (InitiatorType::Beacon, self.url.clone())
720 }
721
722 fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
723 self.global.root()
724 }
725}