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};
15use js::context::JSContext;
16use js::rust::MutableHandleValue;
17use net_traits::blob_url_store::UrlWithBlobClaim;
18use net_traits::request::{
19 CredentialsMode, Destination, RequestBuilder, RequestId, RequestMode,
20 is_cors_safelisted_request_content_type,
21};
22use net_traits::{FetchMetadata, NetworkError, ResourceFetchTiming};
23use regex::Regex;
24#[cfg(feature = "gamepad")]
25use script_bindings::cell::DomRefCell;
26use script_bindings::reflector::{Reflector, reflect_dom_object_with_cx};
27use servo_base::generic_channel;
28use servo_config::pref;
29use servo_url::ServoUrl;
30
31use crate::body::Extractable;
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::RequestWithGlobalScope;
70use crate::network_listener::{FetchResponseListener, ResourceTimingListener, submit_timing};
71
72pub(crate) fn hardware_concurrency() -> u64 {
73 static CPUS: LazyLock<u64> = LazyLock::new(|| num_cpus::get().try_into().unwrap_or(1));
74
75 *CPUS
76}
77
78static SAFELISTED_SCHEMES: [&str; 24] = [
80 "bitcoin",
81 "ftp",
82 "ftps",
83 "geo",
84 "im",
85 "irc",
86 "ircs",
87 "magnet",
88 "mailto",
89 "matrix",
90 "mms",
91 "news",
92 "nntp",
93 "openpgp4fpr",
94 "sftp",
95 "sip",
96 "sms",
97 "smsto",
98 "ssh",
99 "tel",
100 "urn",
101 "webcal",
102 "wtai",
103 "xmpp",
104];
105
106fn matches_web_plus_protocol(scheme: &str) -> bool {
108 static WEB_PLUS_SCHEME_GRAMMAR: LazyLock<Regex> =
109 LazyLock::new(|| Regex::new(r#"^web\+[a-z]+$"#).unwrap());
110
111 WEB_PLUS_SCHEME_GRAMMAR.is_match(scheme)
112}
113
114#[dom_struct]
115pub(crate) struct Navigator {
116 reflector_: Reflector,
117 #[cfg(feature = "bluetooth")]
118 bluetooth: MutNullableDom<Bluetooth>,
119 credentials: MutNullableDom<CredentialsContainer>,
120 plugins: MutNullableDom<PluginArray>,
121 mime_types: MutNullableDom<MimeTypeArray>,
122 service_worker: MutNullableDom<ServiceWorkerContainer>,
123 #[cfg(feature = "webxr")]
124 xr: MutNullableDom<XRSystem>,
125 mediadevices: MutNullableDom<MediaDevices>,
126 #[cfg(feature = "gamepad")]
128 gamepads: DomRefCell<Vec<MutNullableDom<Gamepad>>>,
129 permissions: MutNullableDom<Permissions>,
130 mediasession: MutNullableDom<MediaSession>,
131 clipboard: MutNullableDom<Clipboard>,
132 storage: MutNullableDom<StorageManager>,
133 #[cfg(feature = "webgpu")]
134 gpu: MutNullableDom<GPU>,
135 #[cfg(feature = "gamepad")]
137 has_gamepad_gesture: Cell<bool>,
138 servo_internals: MutNullableDom<ServoInternals>,
139 user_activation: MutNullableDom<UserActivation>,
140 wake_lock: MutNullableDom<WakeLock>,
141}
142
143impl Navigator {
144 fn new_inherited() -> Navigator {
145 Navigator {
146 reflector_: Reflector::new(),
147 #[cfg(feature = "bluetooth")]
148 bluetooth: Default::default(),
149 credentials: Default::default(),
150 plugins: Default::default(),
151 mime_types: Default::default(),
152 service_worker: Default::default(),
153 #[cfg(feature = "webxr")]
154 xr: Default::default(),
155 mediadevices: Default::default(),
156 #[cfg(feature = "gamepad")]
157 gamepads: Default::default(),
158 permissions: Default::default(),
159 mediasession: Default::default(),
160 clipboard: Default::default(),
161 storage: Default::default(),
162 #[cfg(feature = "webgpu")]
163 gpu: Default::default(),
164 #[cfg(feature = "gamepad")]
165 has_gamepad_gesture: Cell::new(false),
166 servo_internals: Default::default(),
167 user_activation: Default::default(),
168 wake_lock: Default::default(),
169 }
170 }
171
172 pub(crate) fn new(cx: &mut JSContext, window: &Window) -> DomRoot<Navigator> {
173 reflect_dom_object_with_cx(Box::new(Navigator::new_inherited()), window, cx)
174 }
175
176 #[cfg(feature = "webxr")]
177 pub(crate) fn xr(&self) -> Option<DomRoot<XRSystem>> {
178 self.xr.get()
179 }
180
181 #[cfg(feature = "gamepad")]
182 pub(crate) fn get_gamepad(&self, index: usize) -> Option<DomRoot<Gamepad>> {
183 self.gamepads.borrow().get(index).and_then(|g| g.get())
184 }
185
186 #[cfg(feature = "gamepad")]
187 pub(crate) fn set_gamepad(&self, index: usize, gamepad: Option<&Gamepad>) {
188 if let Some(gamepad_to_set) = self.gamepads.borrow().get(index) {
189 gamepad_to_set.set(gamepad);
190 }
191 }
192
193 #[cfg(feature = "gamepad")]
195 pub(crate) fn select_gamepad_index(&self) -> u32 {
196 let mut gamepad_list = self.gamepads.borrow_mut();
197 if let Some(index) = gamepad_list.iter().position(|g| g.get().is_none()) {
198 index as u32
199 } else {
200 let len = gamepad_list.len();
201 gamepad_list.resize_with(len + 1, Default::default);
202 len as u32
203 }
204 }
205
206 #[cfg(feature = "gamepad")]
208 pub(crate) fn shrink_gamepads_list(&self) {
209 let mut gamepad_list = self.gamepads.borrow_mut();
210 for i in (0..gamepad_list.len()).rev() {
211 if gamepad_list.get(i).is_none() {
212 gamepad_list.remove(i);
213 } else {
214 break;
215 }
216 }
217 }
218
219 #[cfg(feature = "gamepad")]
220 pub(crate) fn get_connected_gamepad(&self) -> Vec<DomRoot<Gamepad>> {
221 self.gamepads
222 .borrow()
223 .iter()
224 .filter_map(|gamepad| gamepad.get())
225 .filter(|gamepad| gamepad.connected())
226 .collect()
227 }
228
229 #[cfg(feature = "gamepad")]
230 pub(crate) fn has_gamepad_gesture(&self) -> bool {
231 self.has_gamepad_gesture.get()
232 }
233
234 #[cfg(feature = "gamepad")]
235 pub(crate) fn set_has_gamepad_gesture(&self, has_gamepad_gesture: bool) {
236 self.has_gamepad_gesture.set(has_gamepad_gesture);
237 }
238
239 fn normalize_protocol_handler_parameters(
241 &self,
242 scheme: DOMString,
243 url: USVString,
244 ) -> Fallible<(String, ServoUrl)> {
245 let scheme = scheme.to_ascii_lowercase();
247 if !SAFELISTED_SCHEMES.contains(&scheme.as_ref()) && !matches_web_plus_protocol(&scheme) {
250 return Err(Error::Security(None));
251 }
252 if !url.contains("%s") {
254 return Err(Error::Syntax(Some(
255 "Missing replacement string %s in URL".to_owned(),
256 )));
257 }
258 let environment = self.global();
260 let window = environment.as_window();
262 let Ok(url) = window.Document().encoding_parse_a_url(&url) else {
263 return Err(Error::Syntax(Some("Cannot parse URL".to_owned())));
265 };
266 if !matches!(url.scheme(), "http" | "https") {
269 return Err(Error::Security(None));
270 }
271 let environment_origin = environment.origin().immutable().clone();
272 if url.origin() != environment_origin {
273 return Err(Error::Security(None));
274 }
275 assert!(url.is_potentially_trustworthy());
277 Ok((scheme, url))
279 }
280
281 fn send_protocol_update_registration_to_embedder(
282 &self,
283 registration: ProtocolHandlerUpdateRegistration,
284 ) {
285 let global = self.global();
286 let window = global.as_window();
287 let (sender, _) = generic_channel::channel().unwrap();
288 let _ = global
289 .script_to_embedder_chan()
290 .send(EmbedderMsg::AllowProtocolHandlerRequest(
291 window.webview_id(),
292 registration,
293 sender,
294 ));
295 }
296}
297
298impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
299 fn Product(&self) -> DOMString {
301 navigatorinfo::Product()
302 }
303
304 fn ProductSub(&self) -> DOMString {
306 navigatorinfo::ProductSub()
307 }
308
309 fn Vendor(&self) -> DOMString {
311 navigatorinfo::Vendor()
312 }
313
314 fn VendorSub(&self) -> DOMString {
316 navigatorinfo::VendorSub()
317 }
318
319 fn TaintEnabled(&self) -> bool {
321 navigatorinfo::TaintEnabled()
322 }
323
324 fn AppName(&self) -> DOMString {
326 navigatorinfo::AppName()
327 }
328
329 fn AppCodeName(&self) -> DOMString {
331 navigatorinfo::AppCodeName()
332 }
333
334 fn Platform(&self) -> DOMString {
336 navigatorinfo::Platform()
337 }
338
339 fn UserAgent(&self) -> DOMString {
341 navigatorinfo::UserAgent(&pref!(user_agent))
342 }
343
344 fn AppVersion(&self) -> DOMString {
346 navigatorinfo::AppVersion()
347 }
348
349 #[cfg(feature = "bluetooth")]
351 fn Bluetooth(&self, cx: &mut js::context::JSContext) -> DomRoot<Bluetooth> {
352 self.bluetooth
353 .or_init(|| Bluetooth::new(cx, &self.global()))
354 }
355
356 fn Credentials(&self, cx: &mut js::context::JSContext) -> DomRoot<CredentialsContainer> {
358 self.credentials
359 .or_init(|| CredentialsContainer::new(cx, &self.global()))
360 }
361
362 fn Geolocation(&self, cx: &mut js::context::JSContext) -> DomRoot<Geolocation> {
364 Geolocation::new(cx, &self.global())
365 }
366
367 fn Language(&self) -> DOMString {
369 navigatorinfo::Language()
370 }
371
372 fn Languages(&self, cx: &mut js::context::JSContext, retval: MutableHandleValue) {
374 to_frozen_array(cx, &[self.Language()], retval)
375 }
376
377 fn OnLine(&self) -> bool {
379 true
380 }
381
382 fn Plugins(&self, cx: &mut JSContext) -> DomRoot<PluginArray> {
384 self.plugins
385 .or_init(|| PluginArray::new(cx, &self.global()))
386 }
387
388 fn MimeTypes(&self, cx: &mut JSContext) -> DomRoot<MimeTypeArray> {
390 self.mime_types
391 .or_init(|| MimeTypeArray::new(cx, &self.global()))
392 }
393
394 fn JavaEnabled(&self) -> bool {
396 false
397 }
398
399 fn PdfViewerEnabled(&self) -> bool {
401 false
402 }
403
404 fn ServiceWorker(&self, cx: &mut js::context::JSContext) -> DomRoot<ServiceWorkerContainer> {
406 self.service_worker
407 .or_init(|| ServiceWorkerContainer::new(cx, &self.global()))
408 }
409
410 fn CookieEnabled(&self) -> bool {
412 true
413 }
414
415 #[cfg(feature = "gamepad")]
417 fn GetGamepads(&self, cx: &mut JSContext) -> Fallible<Vec<Option<DomRoot<Gamepad>>>> {
418 use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods;
419
420 let global = self.global();
422 let window = global.as_window();
423 let doc = window.Document();
424
425 if !doc.is_fully_active() {
427 return Ok(Vec::new());
428 }
429
430 if !doc.allowed_to_use_feature(PermissionName::Gamepad) {
433 return Err(Error::Security(Some(
434 "Gamepad permission not allowed".into(),
435 )));
436 }
437
438 if !self.has_gamepad_gesture.get() {
440 return Ok(Vec::new());
441 }
442
443 let now = *window.Performance(cx).Now();
445
446 Ok(self
449 .gamepads
450 .borrow()
451 .iter()
452 .map(|slot| {
453 slot.get().inspect(|gamepad| {
454 if !gamepad.exposed() {
456 gamepad.set_exposed(true);
458 gamepad.update_timestamp(now);
460 }
461 })
462 })
463 .collect()) }
466 fn Permissions(&self, cx: &mut JSContext) -> DomRoot<Permissions> {
468 self.permissions
469 .or_init(|| Permissions::new(cx, &self.global()))
470 }
471
472 #[cfg(feature = "webxr")]
474 fn Xr(&self, cx: &mut JSContext) -> DomRoot<XRSystem> {
475 self.xr
476 .or_init(|| XRSystem::new(cx, self.global().as_window()))
477 }
478
479 fn MediaDevices(&self, cx: &mut JSContext) -> DomRoot<MediaDevices> {
481 self.mediadevices
482 .or_init(|| MediaDevices::new(cx, &self.global()))
483 }
484
485 fn MediaSession(&self, cx: &mut JSContext) -> DomRoot<MediaSession> {
487 self.mediasession.or_init(|| {
488 let global = self.global();
496 let window = global.as_window();
497 MediaSession::new(cx, window)
498 })
499 }
500
501 #[cfg(feature = "webgpu")]
503 fn Gpu(&self, cx: &mut JSContext) -> DomRoot<GPU> {
504 self.gpu.or_init(|| GPU::new(cx, &self.global()))
505 }
506
507 fn HardwareConcurrency(&self) -> u64 {
509 hardware_concurrency()
510 }
511
512 fn Clipboard(&self, cx: &mut js::context::JSContext) -> DomRoot<Clipboard> {
514 self.clipboard
515 .or_init(|| Clipboard::new(cx, &self.global()))
516 }
517
518 fn Storage(&self, cx: &mut js::context::JSContext) -> DomRoot<StorageManager> {
520 self.storage
521 .or_init(|| StorageManager::new(cx, &self.global()))
522 }
523
524 fn SendBeacon(
526 &self,
527 cx: &mut js::context::JSContext,
528 url: USVString,
529 data: Option<BodyInit>,
530 ) -> Fallible<bool> {
531 let global = self.global();
532 let base = global.api_base_url();
534 let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
542 return Err(Error::Type(c"Cannot parse URL".to_owned()));
543 };
544 if !matches!(url.scheme(), "http" | "https") {
545 return Err(Error::Type(c"URL is not http(s)".to_owned()));
546 }
547 let mut request_body = None;
548 let mut headers = HeaderMap::with_capacity(1);
550 let mut cors_mode = RequestMode::NoCors;
552 if let Some(data) = data {
554 let extracted_body = data.extract(cx, &global, true)?;
557 if let Some(total_bytes) = extracted_body.total_bytes {
561 let in_flight_keep_alive_bytes =
562 global.total_size_of_in_flight_keep_alive_records();
563 if total_bytes as u64 + in_flight_keep_alive_bytes > 64 * 1024 {
564 return Ok(false);
565 }
566 }
567 if let Some(content_type) = extracted_body.content_type.as_ref() {
569 cors_mode = RequestMode::CorsMode;
571 if is_cors_safelisted_request_content_type(content_type.as_bytes().deref()) {
574 cors_mode = RequestMode::NoCors;
575 }
576 if let Ok(content_type_header_value) = HeaderValue::from_str(&content_type.str()) {
581 headers.insert(header::CONTENT_TYPE, content_type_header_value);
582 }
583 }
584 request_body = Some(extracted_body.into_net_request_body(cx).0);
585 }
586 let request = RequestBuilder::new(
588 None,
589 UrlWithBlobClaim::from_url_without_having_claimed_blob(url.clone()),
590 global.get_referrer(),
591 )
592 .mode(cors_mode)
593 .destination(Destination::None)
594 .with_global_scope(&global)
595 .method(http::Method::POST)
596 .body(request_body)
597 .keep_alive(true)
598 .credentials_mode(CredentialsMode::Include)
599 .headers(headers);
600 global.fetch(
602 request,
603 BeaconFetchListener {
604 url,
605 global: Trusted::new(&global),
606 },
607 global.task_manager().networking_task_source().into(),
608 );
609 Ok(true)
612 }
613
614 fn Servo(&self, cx: &mut js::context::JSContext) -> DomRoot<ServoInternals> {
616 self.servo_internals
617 .or_init(|| ServoInternals::new(cx, &self.global()))
618 }
619
620 fn RegisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
622 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
625 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
637 scheme,
638 url,
639 register_or_unregister: RegisterOrUnregister::Register,
640 });
641 Ok(())
642 }
643
644 fn UnregisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
646 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
649 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
651 scheme,
652 url,
653 register_or_unregister: RegisterOrUnregister::Unregister,
654 });
655 Ok(())
656 }
657
658 fn UserActivation(&self, cx: &mut js::context::JSContext) -> DomRoot<UserActivation> {
660 self.user_activation
661 .or_init(|| UserActivation::new(cx, &self.global()))
662 }
663
664 fn WakeLock(&self, cx: &mut js::context::JSContext) -> DomRoot<WakeLock> {
666 self.wake_lock.or_init(|| WakeLock::new(cx, &self.global()))
667 }
668}
669
670struct BeaconFetchListener {
671 url: ServoUrl,
673 global: Trusted<GlobalScope>,
675}
676
677impl FetchResponseListener for BeaconFetchListener {
678 fn process_request_body(&mut self, _: RequestId) {}
679
680 fn process_response(
681 &mut self,
682 _: &mut js::context::JSContext,
683 _: RequestId,
684 fetch_metadata: Result<FetchMetadata, NetworkError>,
685 ) {
686 _ = fetch_metadata;
687 }
688
689 fn process_response_chunk(
690 &mut self,
691 _: &mut js::context::JSContext,
692 _: RequestId,
693 chunk: Vec<u8>,
694 ) {
695 _ = chunk;
696 }
697
698 fn process_response_eof(
699 self,
700 cx: &mut js::context::JSContext,
701 _: RequestId,
702 response: Result<(), NetworkError>,
703 timing: ResourceFetchTiming,
704 ) {
705 submit_timing(cx, &self, &response, &timing);
706 }
707
708 fn process_csp_violations(
709 &mut self,
710 cx: &mut js::context::JSContext,
711 _request_id: RequestId,
712 violations: Vec<Violation>,
713 ) {
714 let global = self.resource_timing_global();
715 global.report_csp_violations(cx, violations, None, None);
716 }
717}
718
719impl ResourceTimingListener for BeaconFetchListener {
720 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
721 (InitiatorType::Beacon, self.url.clone())
722 }
723
724 fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
725 self.global.root()
726 }
727}