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};
28use servo_base::generic_channel;
29use servo_config::pref;
30use servo_url::ServoUrl;
31
32use crate::ScriptThread;
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::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 geolocation: MutNullableDom<Geolocation>,
130 #[cfg(feature = "gamepad")]
132 gamepads: DomRefCell<Vec<MutNullableDom<Gamepad>>>,
133 permissions: MutNullableDom<Permissions>,
134 mediasession: MutNullableDom<MediaSession>,
135 clipboard: MutNullableDom<Clipboard>,
136 storage: MutNullableDom<StorageManager>,
137 #[cfg(feature = "webgpu")]
138 gpu: MutNullableDom<GPU>,
139 #[cfg(feature = "gamepad")]
141 has_gamepad_gesture: Cell<bool>,
142 servo_internals: MutNullableDom<ServoInternals>,
143 user_activation: MutNullableDom<UserActivation>,
144 wake_lock: MutNullableDom<WakeLock>,
145}
146
147impl Navigator {
148 fn new_inherited() -> Navigator {
149 Navigator {
150 reflector_: Reflector::new(),
151 #[cfg(feature = "bluetooth")]
152 bluetooth: Default::default(),
153 credentials: Default::default(),
154 plugins: Default::default(),
155 mime_types: Default::default(),
156 service_worker: Default::default(),
157 #[cfg(feature = "webxr")]
158 xr: Default::default(),
159 mediadevices: Default::default(),
160 geolocation: Default::default(),
161 #[cfg(feature = "gamepad")]
162 gamepads: Default::default(),
163 permissions: Default::default(),
164 mediasession: Default::default(),
165 clipboard: Default::default(),
166 storage: Default::default(),
167 #[cfg(feature = "webgpu")]
168 gpu: Default::default(),
169 #[cfg(feature = "gamepad")]
170 has_gamepad_gesture: Cell::new(false),
171 servo_internals: Default::default(),
172 user_activation: Default::default(),
173 wake_lock: Default::default(),
174 }
175 }
176
177 pub(crate) fn new(cx: &mut JSContext, window: &Window) -> DomRoot<Navigator> {
178 reflect_dom_object(cx, Box::new(Navigator::new_inherited()), window)
179 }
180
181 #[cfg(feature = "webxr")]
182 pub(crate) fn xr(&self) -> Option<DomRoot<XRSystem>> {
183 self.xr.get()
184 }
185
186 pub(crate) fn maybe_geolocation(&self) -> Option<DomRoot<Geolocation>> {
188 self.geolocation.get()
189 }
190
191 #[cfg(feature = "gamepad")]
192 pub(crate) fn get_gamepad(&self, index: usize) -> Option<DomRoot<Gamepad>> {
193 self.gamepads.borrow().get(index).and_then(|g| g.get())
194 }
195
196 #[cfg(feature = "gamepad")]
197 pub(crate) fn set_gamepad(&self, index: usize, gamepad: Option<&Gamepad>) {
198 if let Some(gamepad_to_set) = self.gamepads.borrow().get(index) {
199 gamepad_to_set.set(gamepad);
200 }
201 }
202
203 #[cfg(feature = "gamepad")]
205 pub(crate) fn select_gamepad_index(&self) -> u32 {
206 let mut gamepad_list = self.gamepads.borrow_mut();
207 if let Some(index) = gamepad_list.iter().position(|g| g.get().is_none()) {
208 index as u32
209 } else {
210 let len = gamepad_list.len();
211 gamepad_list.resize_with(len + 1, Default::default);
212 len as u32
213 }
214 }
215
216 #[cfg(feature = "gamepad")]
218 pub(crate) fn shrink_gamepads_list(&self) {
219 let mut gamepad_list = self.gamepads.borrow_mut();
220 for i in (0..gamepad_list.len()).rev() {
221 if gamepad_list.get(i).is_none() {
222 gamepad_list.remove(i);
223 } else {
224 break;
225 }
226 }
227 }
228
229 #[cfg(feature = "gamepad")]
230 pub(crate) fn get_connected_gamepad(&self) -> Vec<DomRoot<Gamepad>> {
231 self.gamepads
232 .borrow()
233 .iter()
234 .filter_map(|gamepad| gamepad.get())
235 .filter(|gamepad| gamepad.connected())
236 .collect()
237 }
238
239 #[cfg(feature = "gamepad")]
240 pub(crate) fn has_gamepad_gesture(&self) -> bool {
241 self.has_gamepad_gesture.get()
242 }
243
244 #[cfg(feature = "gamepad")]
245 pub(crate) fn set_has_gamepad_gesture(&self, has_gamepad_gesture: bool) {
246 self.has_gamepad_gesture.set(has_gamepad_gesture);
247 }
248
249 fn normalize_protocol_handler_parameters(
251 &self,
252 scheme: DOMString,
253 url: USVString,
254 ) -> Fallible<(String, ServoUrl)> {
255 let scheme = scheme.to_ascii_lowercase();
257 if !SAFELISTED_SCHEMES.contains(&scheme.as_ref()) && !matches_web_plus_protocol(&scheme) {
260 return Err(Error::Security(None));
261 }
262 if !url.contains("%s") {
264 return Err(Error::Syntax(Some(
265 "Missing replacement string %s in URL".to_owned(),
266 )));
267 }
268 let environment = self.global();
270 let window = environment.as_window();
272 let Ok(url) = window.Document().encoding_parse_a_url(&url) else {
273 return Err(Error::Syntax(Some("Cannot parse URL".to_owned())));
275 };
276 if !matches!(url.scheme(), "http" | "https") {
279 return Err(Error::Security(None));
280 }
281 let environment_origin = environment.origin().immutable().clone();
282 if url.origin() != environment_origin {
283 return Err(Error::Security(None));
284 }
285 assert!(url.is_potentially_trustworthy());
287 Ok((scheme, url))
289 }
290
291 fn send_protocol_update_registration_to_embedder(
292 &self,
293 registration: ProtocolHandlerUpdateRegistration,
294 ) {
295 let global = self.global();
296 let window = global.as_window();
297 let (sender, _) = generic_channel::channel().unwrap();
298 let _ = global
299 .script_to_embedder_chan()
300 .send(EmbedderMsg::AllowProtocolHandlerRequest(
301 window.webview_id(),
302 registration,
303 sender,
304 ));
305 }
306}
307
308impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
309 fn Product(&self) -> DOMString {
311 navigatorinfo::Product()
312 }
313
314 fn ProductSub(&self) -> DOMString {
316 navigatorinfo::ProductSub()
317 }
318
319 fn Vendor(&self) -> DOMString {
321 navigatorinfo::Vendor()
322 }
323
324 fn VendorSub(&self) -> DOMString {
326 navigatorinfo::VendorSub()
327 }
328
329 fn TaintEnabled(&self) -> bool {
331 navigatorinfo::TaintEnabled()
332 }
333
334 fn AppName(&self) -> DOMString {
336 navigatorinfo::AppName()
337 }
338
339 fn AppCodeName(&self) -> DOMString {
341 navigatorinfo::AppCodeName()
342 }
343
344 fn Platform(&self) -> DOMString {
346 navigatorinfo::Platform()
347 }
348
349 fn UserAgent(&self) -> DOMString {
351 navigatorinfo::UserAgent(&pref!(user_agent))
352 }
353
354 fn AppVersion(&self) -> DOMString {
356 navigatorinfo::AppVersion()
357 }
358
359 #[cfg(feature = "bluetooth")]
361 fn Bluetooth(&self, cx: &mut js::context::JSContext) -> DomRoot<Bluetooth> {
362 self.bluetooth
363 .or_init(|| Bluetooth::new(cx, &self.global()))
364 }
365
366 fn Credentials(&self, cx: &mut js::context::JSContext) -> DomRoot<CredentialsContainer> {
368 self.credentials
369 .or_init(|| CredentialsContainer::new(cx, &self.global()))
370 }
371
372 fn Geolocation(&self, cx: &mut js::context::JSContext) -> DomRoot<Geolocation> {
374 self.geolocation
375 .or_init(|| Geolocation::new(cx, &self.global()))
376 }
377
378 fn Language(&self) -> DOMString {
380 navigatorinfo::Language()
381 }
382
383 fn Languages(&self, cx: &mut js::context::JSContext, retval: MutableHandleValue) {
385 to_frozen_array(cx, &[self.Language()], retval)
386 }
387
388 fn OnLine(&self) -> bool {
390 ScriptThread::online()
391 }
392
393 fn Plugins(&self, cx: &mut JSContext) -> DomRoot<PluginArray> {
395 self.plugins
396 .or_init(|| PluginArray::new(cx, &self.global()))
397 }
398
399 fn MimeTypes(&self, cx: &mut JSContext) -> DomRoot<MimeTypeArray> {
401 self.mime_types
402 .or_init(|| MimeTypeArray::new(cx, &self.global()))
403 }
404
405 fn JavaEnabled(&self) -> bool {
407 false
408 }
409
410 fn PdfViewerEnabled(&self) -> bool {
412 false
413 }
414
415 fn ServiceWorker(&self, cx: &mut js::context::JSContext) -> DomRoot<ServiceWorkerContainer> {
417 self.service_worker
418 .or_init(|| ServiceWorkerContainer::new(cx, &self.global()))
419 }
420
421 fn CookieEnabled(&self) -> bool {
423 true
424 }
425
426 #[cfg(feature = "gamepad")]
428 fn GetGamepads(&self, cx: &mut JSContext) -> Fallible<Vec<Option<DomRoot<Gamepad>>>> {
429 use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods;
430
431 let global = self.global();
433 let window = global.as_window();
434 let doc = window.Document();
435
436 if !doc.is_fully_active() {
438 return Ok(Vec::new());
439 }
440
441 if !doc.allowed_to_use_feature(PermissionName::Gamepad) {
444 return Err(Error::Security(Some(
445 "Gamepad permission not allowed".into(),
446 )));
447 }
448
449 if !self.has_gamepad_gesture.get() {
451 return Ok(Vec::new());
452 }
453
454 let now = *window.Performance(cx).Now();
456
457 Ok(self
460 .gamepads
461 .borrow()
462 .iter()
463 .map(|slot| {
464 slot.get().inspect(|gamepad| {
465 if !gamepad.exposed() {
467 gamepad.set_exposed(true);
469 gamepad.update_timestamp(now);
471 }
472 })
473 })
474 .collect()) }
477 fn Permissions(&self, cx: &mut JSContext) -> DomRoot<Permissions> {
479 self.permissions
480 .or_init(|| Permissions::new(cx, &self.global()))
481 }
482
483 #[cfg(feature = "webxr")]
485 fn Xr(&self, cx: &mut JSContext) -> DomRoot<XRSystem> {
486 self.xr
487 .or_init(|| XRSystem::new(cx, self.global().as_window()))
488 }
489
490 fn MediaDevices(&self, cx: &mut JSContext) -> DomRoot<MediaDevices> {
492 self.mediadevices
493 .or_init(|| MediaDevices::new(cx, &self.global()))
494 }
495
496 fn MediaSession(&self, cx: &mut JSContext) -> DomRoot<MediaSession> {
498 self.mediasession.or_init(|| {
499 let global = self.global();
507 let window = global.as_window();
508 MediaSession::new(cx, window)
509 })
510 }
511
512 #[cfg(feature = "webgpu")]
514 fn Gpu(&self, cx: &mut JSContext) -> DomRoot<GPU> {
515 self.gpu.or_init(|| GPU::new(cx, &self.global()))
516 }
517
518 fn HardwareConcurrency(&self) -> u64 {
520 hardware_concurrency()
521 }
522
523 fn Clipboard(&self, cx: &mut js::context::JSContext) -> DomRoot<Clipboard> {
525 self.clipboard
526 .or_init(|| Clipboard::new(cx, &self.global()))
527 }
528
529 fn Storage(&self, cx: &mut js::context::JSContext) -> DomRoot<StorageManager> {
531 self.storage
532 .or_init(|| StorageManager::new(cx, &self.global()))
533 }
534
535 fn SendBeacon(
537 &self,
538 cx: &mut js::context::JSContext,
539 url: USVString,
540 data: Option<BodyInit>,
541 ) -> Fallible<bool> {
542 let global = self.global();
543 let base = global.api_base_url();
545 let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
553 return Err(Error::Type(c"Cannot parse URL".to_owned()));
554 };
555 if !matches!(url.scheme(), "http" | "https") {
556 return Err(Error::Type(c"URL is not http(s)".to_owned()));
557 }
558 let mut request_body = None;
559 let mut headers = HeaderMap::with_capacity(1);
561 let mut cors_mode = RequestMode::NoCors;
563 if let Some(data) = data {
565 let extracted_body = data.extract(cx, &global, true)?;
568 if let Some(total_bytes) = extracted_body.total_bytes {
572 let in_flight_keep_alive_bytes =
573 global.total_size_of_in_flight_keep_alive_records();
574 if total_bytes as u64 + in_flight_keep_alive_bytes > 64 * 1024 {
575 return Ok(false);
576 }
577 }
578 if let Some(content_type) = extracted_body.content_type.as_ref() {
580 cors_mode = RequestMode::CorsMode;
582 if is_cors_safelisted_request_content_type(
585 content_type.as_bytes(cx.no_gc()).deref(),
586 ) {
587 cors_mode = RequestMode::NoCors;
588 }
589 if let Ok(content_type_header_value) = HeaderValue::from_str(&content_type.str()) {
594 headers.insert(header::CONTENT_TYPE, content_type_header_value);
595 }
596 }
597 request_body = Some(extracted_body.into_net_request_body(cx).0);
598 }
599 let request = RequestBuilder::new(
601 None,
602 UrlWithBlobClaim::from_url_without_having_claimed_blob(url.clone()),
603 global.get_referrer(),
604 )
605 .mode(cors_mode)
606 .destination(Destination::None)
607 .with_global_scope(&global)
608 .method(http::Method::POST)
609 .body(request_body)
610 .keep_alive(true)
611 .credentials_mode(CredentialsMode::Include)
612 .headers(headers);
613 global.fetch(
615 request,
616 BeaconFetchListener { url },
617 global.task_manager().networking_task_source().into(),
618 );
619 Ok(true)
622 }
623
624 fn Servo(&self, cx: &mut js::context::JSContext) -> DomRoot<ServoInternals> {
626 self.servo_internals
627 .or_init(|| ServoInternals::new(cx, &self.global()))
628 }
629
630 fn RegisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
632 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
635 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
647 scheme,
648 url,
649 register_or_unregister: RegisterOrUnregister::Register,
650 });
651 Ok(())
652 }
653
654 fn UnregisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
656 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
659 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
661 scheme,
662 url,
663 register_or_unregister: RegisterOrUnregister::Unregister,
664 });
665 Ok(())
666 }
667
668 fn UserActivation(&self, cx: &mut js::context::JSContext) -> DomRoot<UserActivation> {
670 self.user_activation
671 .or_init(|| UserActivation::new(cx, &self.global()))
672 }
673
674 fn WakeLock(&self, cx: &mut js::context::JSContext) -> DomRoot<WakeLock> {
676 self.wake_lock.or_init(|| WakeLock::new(cx, &self.global()))
677 }
678}
679
680struct BeaconFetchListener {
681 url: ServoUrl,
683}
684
685impl FetchResponseListener for BeaconFetchListener {
686 fn process_request_body(&mut self, _: &GlobalScope, _: RequestId) {}
687
688 fn process_response(
689 &mut self,
690 _: &mut js::context::JSContext,
691 _: &GlobalScope,
692 _: RequestId,
693 fetch_metadata: Result<FetchMetadata, NetworkError>,
694 ) {
695 _ = fetch_metadata;
696 }
697
698 fn process_response_chunk(
699 &mut self,
700 _: &mut js::context::JSContext,
701 _: &GlobalScope,
702 _: RequestId,
703 chunk: Bytes,
704 ) {
705 _ = chunk;
706 }
707
708 fn process_response_eof(
709 self,
710 cx: &mut js::context::JSContext,
711 global: &GlobalScope,
712 _: RequestId,
713 response: Result<(), NetworkError>,
714 timing: ResourceFetchTiming,
715 ) {
716 submit_timing(cx, global, &self, &response, &timing);
717 }
718
719 fn process_csp_violations(
720 &mut self,
721 cx: &mut js::context::JSContext,
722 global: &GlobalScope,
723 _request_id: RequestId,
724 violations: Vec<Violation>,
725 ) {
726 global.report_csp_violations(cx, violations, None, None);
727 }
728}
729
730impl ResourceTimingListener for BeaconFetchListener {
731 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
732 (InitiatorType::Beacon, self.url.clone())
733 }
734}