1#[cfg(feature = "gamepad")]
6use std::cell::Cell;
7use std::convert::TryInto;
8use std::ops::Deref;
9use std::sync::LazyLock;
10
11use bytes::Bytes;
12use dom_struct::dom_struct;
13use embedder_traits::{EmbedderMsg, ProtocolHandlerUpdateRegistration, RegisterOrUnregister};
14use headers::HeaderMap;
15use http::header::{self, HeaderValue};
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_with_cx};
28use servo_base::generic_channel;
29use servo_config::pref;
30use servo_url::ServoUrl;
31
32use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods;
33#[cfg(feature = "gamepad")]
34use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::PermissionName;
35use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
36use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit;
37use crate::dom::bindings::error::{Error, Fallible};
38use crate::dom::bindings::refcounted::Trusted;
39use crate::dom::bindings::reflector::DomGlobal;
40use crate::dom::bindings::root::{DomRoot, MutNullableDom};
41use crate::dom::bindings::str::{DOMString, USVString};
42use crate::dom::bindings::utils::to_frozen_array;
43#[cfg(feature = "bluetooth")]
44use crate::dom::bluetooth::Bluetooth;
45use crate::dom::clipboard::Clipboard;
46use crate::dom::credentialmanagement::credentialscontainer::CredentialsContainer;
47use crate::dom::csp::{GlobalCspReporting, Violation};
48#[cfg(feature = "gamepad")]
49use crate::dom::gamepad::Gamepad;
50use crate::dom::geolocation::Geolocation;
51use crate::dom::globalscope::GlobalScope;
52use crate::dom::mediadevices::MediaDevices;
53use crate::dom::mediasession::MediaSession;
54use crate::dom::mimetypearray::MimeTypeArray;
55use crate::dom::navigatorinfo;
56use crate::dom::performance::performanceresourcetiming::InitiatorType;
57use crate::dom::permissions::Permissions;
58use crate::dom::pluginarray::PluginArray;
59use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
60use crate::dom::servointernals::ServoInternals;
61use crate::dom::storagemanager::StorageManager;
62use crate::dom::types::UserActivation;
63use crate::dom::wakelock::WakeLock;
64#[cfg(feature = "webgpu")]
65use crate::dom::webgpu::gpu::GPU;
66use crate::dom::window::Window;
67#[cfg(feature = "webxr")]
68use crate::dom::xrsystem::XRSystem;
69use crate::fetch::body::Extractable;
70use crate::fetch::fetch::RequestWithGlobalScope;
71use crate::fetch::network_listener::{
72 FetchResponseListener, ResourceTimingListener, submit_timing,
73};
74
75pub(crate) fn hardware_concurrency() -> u64 {
76 static CPUS: LazyLock<u64> = LazyLock::new(|| num_cpus::get().try_into().unwrap_or(1));
77
78 *CPUS
79}
80
81static SAFELISTED_SCHEMES: [&str; 24] = [
83 "bitcoin",
84 "ftp",
85 "ftps",
86 "geo",
87 "im",
88 "irc",
89 "ircs",
90 "magnet",
91 "mailto",
92 "matrix",
93 "mms",
94 "news",
95 "nntp",
96 "openpgp4fpr",
97 "sftp",
98 "sip",
99 "sms",
100 "smsto",
101 "ssh",
102 "tel",
103 "urn",
104 "webcal",
105 "wtai",
106 "xmpp",
107];
108
109fn matches_web_plus_protocol(scheme: &str) -> bool {
111 static WEB_PLUS_SCHEME_GRAMMAR: LazyLock<Regex> =
112 LazyLock::new(|| Regex::new(r#"^web\+[a-z]+$"#).unwrap());
113
114 WEB_PLUS_SCHEME_GRAMMAR.is_match(scheme)
115}
116
117#[dom_struct]
118pub(crate) struct Navigator {
119 reflector_: Reflector,
120 #[cfg(feature = "bluetooth")]
121 bluetooth: MutNullableDom<Bluetooth>,
122 credentials: MutNullableDom<CredentialsContainer>,
123 plugins: MutNullableDom<PluginArray>,
124 mime_types: MutNullableDom<MimeTypeArray>,
125 service_worker: MutNullableDom<ServiceWorkerContainer>,
126 #[cfg(feature = "webxr")]
127 xr: MutNullableDom<XRSystem>,
128 mediadevices: MutNullableDom<MediaDevices>,
129 #[cfg(feature = "gamepad")]
131 gamepads: DomRefCell<Vec<MutNullableDom<Gamepad>>>,
132 permissions: MutNullableDom<Permissions>,
133 mediasession: MutNullableDom<MediaSession>,
134 clipboard: MutNullableDom<Clipboard>,
135 storage: MutNullableDom<StorageManager>,
136 #[cfg(feature = "webgpu")]
137 gpu: MutNullableDom<GPU>,
138 #[cfg(feature = "gamepad")]
140 has_gamepad_gesture: Cell<bool>,
141 servo_internals: MutNullableDom<ServoInternals>,
142 user_activation: MutNullableDom<UserActivation>,
143 wake_lock: MutNullableDom<WakeLock>,
144}
145
146impl Navigator {
147 fn new_inherited() -> Navigator {
148 Navigator {
149 reflector_: Reflector::new(),
150 #[cfg(feature = "bluetooth")]
151 bluetooth: Default::default(),
152 credentials: Default::default(),
153 plugins: Default::default(),
154 mime_types: Default::default(),
155 service_worker: Default::default(),
156 #[cfg(feature = "webxr")]
157 xr: Default::default(),
158 mediadevices: Default::default(),
159 #[cfg(feature = "gamepad")]
160 gamepads: Default::default(),
161 permissions: Default::default(),
162 mediasession: Default::default(),
163 clipboard: Default::default(),
164 storage: Default::default(),
165 #[cfg(feature = "webgpu")]
166 gpu: Default::default(),
167 #[cfg(feature = "gamepad")]
168 has_gamepad_gesture: Cell::new(false),
169 servo_internals: Default::default(),
170 user_activation: Default::default(),
171 wake_lock: Default::default(),
172 }
173 }
174
175 pub(crate) fn new(cx: &mut JSContext, window: &Window) -> DomRoot<Navigator> {
176 reflect_dom_object_with_cx(Box::new(Navigator::new_inherited()), window, cx)
177 }
178
179 #[cfg(feature = "webxr")]
180 pub(crate) fn xr(&self) -> Option<DomRoot<XRSystem>> {
181 self.xr.get()
182 }
183
184 #[cfg(feature = "gamepad")]
185 pub(crate) fn get_gamepad(&self, index: usize) -> Option<DomRoot<Gamepad>> {
186 self.gamepads.borrow().get(index).and_then(|g| g.get())
187 }
188
189 #[cfg(feature = "gamepad")]
190 pub(crate) fn set_gamepad(&self, index: usize, gamepad: Option<&Gamepad>) {
191 if let Some(gamepad_to_set) = self.gamepads.borrow().get(index) {
192 gamepad_to_set.set(gamepad);
193 }
194 }
195
196 #[cfg(feature = "gamepad")]
198 pub(crate) fn select_gamepad_index(&self) -> u32 {
199 let mut gamepad_list = self.gamepads.borrow_mut();
200 if let Some(index) = gamepad_list.iter().position(|g| g.get().is_none()) {
201 index as u32
202 } else {
203 let len = gamepad_list.len();
204 gamepad_list.resize_with(len + 1, Default::default);
205 len as u32
206 }
207 }
208
209 #[cfg(feature = "gamepad")]
211 pub(crate) fn shrink_gamepads_list(&self) {
212 let mut gamepad_list = self.gamepads.borrow_mut();
213 for i in (0..gamepad_list.len()).rev() {
214 if gamepad_list.get(i).is_none() {
215 gamepad_list.remove(i);
216 } else {
217 break;
218 }
219 }
220 }
221
222 #[cfg(feature = "gamepad")]
223 pub(crate) fn get_connected_gamepad(&self) -> Vec<DomRoot<Gamepad>> {
224 self.gamepads
225 .borrow()
226 .iter()
227 .filter_map(|gamepad| gamepad.get())
228 .filter(|gamepad| gamepad.connected())
229 .collect()
230 }
231
232 #[cfg(feature = "gamepad")]
233 pub(crate) fn has_gamepad_gesture(&self) -> bool {
234 self.has_gamepad_gesture.get()
235 }
236
237 #[cfg(feature = "gamepad")]
238 pub(crate) fn set_has_gamepad_gesture(&self, has_gamepad_gesture: bool) {
239 self.has_gamepad_gesture.set(has_gamepad_gesture);
240 }
241
242 fn normalize_protocol_handler_parameters(
244 &self,
245 scheme: DOMString,
246 url: USVString,
247 ) -> Fallible<(String, ServoUrl)> {
248 let scheme = scheme.to_ascii_lowercase();
250 if !SAFELISTED_SCHEMES.contains(&scheme.as_ref()) && !matches_web_plus_protocol(&scheme) {
253 return Err(Error::Security(None));
254 }
255 if !url.contains("%s") {
257 return Err(Error::Syntax(Some(
258 "Missing replacement string %s in URL".to_owned(),
259 )));
260 }
261 let environment = self.global();
263 let window = environment.as_window();
265 let Ok(url) = window.Document().encoding_parse_a_url(&url) else {
266 return Err(Error::Syntax(Some("Cannot parse URL".to_owned())));
268 };
269 if !matches!(url.scheme(), "http" | "https") {
272 return Err(Error::Security(None));
273 }
274 let environment_origin = environment.origin().immutable().clone();
275 if url.origin() != environment_origin {
276 return Err(Error::Security(None));
277 }
278 assert!(url.is_potentially_trustworthy());
280 Ok((scheme, url))
282 }
283
284 fn send_protocol_update_registration_to_embedder(
285 &self,
286 registration: ProtocolHandlerUpdateRegistration,
287 ) {
288 let global = self.global();
289 let window = global.as_window();
290 let (sender, _) = generic_channel::channel().unwrap();
291 let _ = global
292 .script_to_embedder_chan()
293 .send(EmbedderMsg::AllowProtocolHandlerRequest(
294 window.webview_id(),
295 registration,
296 sender,
297 ));
298 }
299}
300
301impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
302 fn Product(&self) -> DOMString {
304 navigatorinfo::Product()
305 }
306
307 fn ProductSub(&self) -> DOMString {
309 navigatorinfo::ProductSub()
310 }
311
312 fn Vendor(&self) -> DOMString {
314 navigatorinfo::Vendor()
315 }
316
317 fn VendorSub(&self) -> DOMString {
319 navigatorinfo::VendorSub()
320 }
321
322 fn TaintEnabled(&self) -> bool {
324 navigatorinfo::TaintEnabled()
325 }
326
327 fn AppName(&self) -> DOMString {
329 navigatorinfo::AppName()
330 }
331
332 fn AppCodeName(&self) -> DOMString {
334 navigatorinfo::AppCodeName()
335 }
336
337 fn Platform(&self) -> DOMString {
339 navigatorinfo::Platform()
340 }
341
342 fn UserAgent(&self) -> DOMString {
344 navigatorinfo::UserAgent(&pref!(user_agent))
345 }
346
347 fn AppVersion(&self) -> DOMString {
349 navigatorinfo::AppVersion()
350 }
351
352 #[cfg(feature = "bluetooth")]
354 fn Bluetooth(&self, cx: &mut js::context::JSContext) -> DomRoot<Bluetooth> {
355 self.bluetooth
356 .or_init(|| Bluetooth::new(cx, &self.global()))
357 }
358
359 fn Credentials(&self, cx: &mut js::context::JSContext) -> DomRoot<CredentialsContainer> {
361 self.credentials
362 .or_init(|| CredentialsContainer::new(cx, &self.global()))
363 }
364
365 fn Geolocation(&self, cx: &mut js::context::JSContext) -> DomRoot<Geolocation> {
367 Geolocation::new(cx, &self.global())
368 }
369
370 fn Language(&self) -> DOMString {
372 navigatorinfo::Language()
373 }
374
375 fn Languages(&self, cx: &mut js::context::JSContext, retval: MutableHandleValue) {
377 to_frozen_array(cx, &[self.Language()], retval)
378 }
379
380 fn OnLine(&self) -> bool {
382 true
383 }
384
385 fn Plugins(&self, cx: &mut JSContext) -> DomRoot<PluginArray> {
387 self.plugins
388 .or_init(|| PluginArray::new(cx, &self.global()))
389 }
390
391 fn MimeTypes(&self, cx: &mut JSContext) -> DomRoot<MimeTypeArray> {
393 self.mime_types
394 .or_init(|| MimeTypeArray::new(cx, &self.global()))
395 }
396
397 fn JavaEnabled(&self) -> bool {
399 false
400 }
401
402 fn PdfViewerEnabled(&self) -> bool {
404 false
405 }
406
407 fn ServiceWorker(&self, cx: &mut js::context::JSContext) -> DomRoot<ServiceWorkerContainer> {
409 self.service_worker
410 .or_init(|| ServiceWorkerContainer::new(cx, &self.global()))
411 }
412
413 fn CookieEnabled(&self) -> bool {
415 true
416 }
417
418 #[cfg(feature = "gamepad")]
420 fn GetGamepads(&self, cx: &mut JSContext) -> Fallible<Vec<Option<DomRoot<Gamepad>>>> {
421 use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods;
422
423 let global = self.global();
425 let window = global.as_window();
426 let doc = window.Document();
427
428 if !doc.is_fully_active() {
430 return Ok(Vec::new());
431 }
432
433 if !doc.allowed_to_use_feature(PermissionName::Gamepad) {
436 return Err(Error::Security(Some(
437 "Gamepad permission not allowed".into(),
438 )));
439 }
440
441 if !self.has_gamepad_gesture.get() {
443 return Ok(Vec::new());
444 }
445
446 let now = *window.Performance(cx).Now();
448
449 Ok(self
452 .gamepads
453 .borrow()
454 .iter()
455 .map(|slot| {
456 slot.get().inspect(|gamepad| {
457 if !gamepad.exposed() {
459 gamepad.set_exposed(true);
461 gamepad.update_timestamp(now);
463 }
464 })
465 })
466 .collect()) }
469 fn Permissions(&self, cx: &mut JSContext) -> DomRoot<Permissions> {
471 self.permissions
472 .or_init(|| Permissions::new(cx, &self.global()))
473 }
474
475 #[cfg(feature = "webxr")]
477 fn Xr(&self, cx: &mut JSContext) -> DomRoot<XRSystem> {
478 self.xr
479 .or_init(|| XRSystem::new(cx, self.global().as_window()))
480 }
481
482 fn MediaDevices(&self, cx: &mut JSContext) -> DomRoot<MediaDevices> {
484 self.mediadevices
485 .or_init(|| MediaDevices::new(cx, &self.global()))
486 }
487
488 fn MediaSession(&self, cx: &mut JSContext) -> DomRoot<MediaSession> {
490 self.mediasession.or_init(|| {
491 let global = self.global();
499 let window = global.as_window();
500 MediaSession::new(cx, window)
501 })
502 }
503
504 #[cfg(feature = "webgpu")]
506 fn Gpu(&self, cx: &mut JSContext) -> DomRoot<GPU> {
507 self.gpu.or_init(|| GPU::new(cx, &self.global()))
508 }
509
510 fn HardwareConcurrency(&self) -> u64 {
512 hardware_concurrency()
513 }
514
515 fn Clipboard(&self, cx: &mut js::context::JSContext) -> DomRoot<Clipboard> {
517 self.clipboard
518 .or_init(|| Clipboard::new(cx, &self.global()))
519 }
520
521 fn Storage(&self, cx: &mut js::context::JSContext) -> DomRoot<StorageManager> {
523 self.storage
524 .or_init(|| StorageManager::new(cx, &self.global()))
525 }
526
527 fn SendBeacon(
529 &self,
530 cx: &mut js::context::JSContext,
531 url: USVString,
532 data: Option<BodyInit>,
533 ) -> Fallible<bool> {
534 let global = self.global();
535 let base = global.api_base_url();
537 let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
545 return Err(Error::Type(c"Cannot parse URL".to_owned()));
546 };
547 if !matches!(url.scheme(), "http" | "https") {
548 return Err(Error::Type(c"URL is not http(s)".to_owned()));
549 }
550 let mut request_body = None;
551 let mut headers = HeaderMap::with_capacity(1);
553 let mut cors_mode = RequestMode::NoCors;
555 if let Some(data) = data {
557 let extracted_body = data.extract(cx, &global, true)?;
560 if let Some(total_bytes) = extracted_body.total_bytes {
564 let in_flight_keep_alive_bytes =
565 global.total_size_of_in_flight_keep_alive_records();
566 if total_bytes as u64 + in_flight_keep_alive_bytes > 64 * 1024 {
567 return Ok(false);
568 }
569 }
570 if let Some(content_type) = extracted_body.content_type.as_ref() {
572 cors_mode = RequestMode::CorsMode;
574 if is_cors_safelisted_request_content_type(content_type.as_bytes().deref()) {
577 cors_mode = RequestMode::NoCors;
578 }
579 if let Ok(content_type_header_value) = HeaderValue::from_str(&content_type.str()) {
584 headers.insert(header::CONTENT_TYPE, content_type_header_value);
585 }
586 }
587 request_body = Some(extracted_body.into_net_request_body(cx).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, cx: &mut js::context::JSContext) -> DomRoot<ServoInternals> {
619 self.servo_internals
620 .or_init(|| ServoInternals::new(cx, &self.global()))
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: Bytes,
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(
712 &mut self,
713 cx: &mut js::context::JSContext,
714 _request_id: RequestId,
715 violations: Vec<Violation>,
716 ) {
717 let global = self.resource_timing_global();
718 global.report_csp_violations(cx, violations, None, None);
719 }
720}
721
722impl ResourceTimingListener for BeaconFetchListener {
723 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
724 (InitiatorType::Beacon, self.url.clone())
725 }
726
727 fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
728 self.global.root()
729 }
730}