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::rust::MutableHandleValue;
16use net_traits::blob_url_store::UrlWithBlobClaim;
17use net_traits::request::{
18 CredentialsMode, Destination, RequestBuilder, RequestId, RequestMode,
19 is_cors_safelisted_request_content_type,
20};
21use net_traits::{FetchMetadata, NetworkError, ResourceFetchTiming};
22use regex::Regex;
23#[cfg(feature = "gamepad")]
24use script_bindings::cell::DomRefCell;
25use script_bindings::reflector::{Reflector, reflect_dom_object};
26use servo_base::generic_channel;
27use servo_config::pref;
28use servo_url::ServoUrl;
29
30use crate::body::Extractable;
31use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods;
32use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
33use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit;
34use crate::dom::bindings::error::{Error, Fallible};
35use crate::dom::bindings::refcounted::Trusted;
36use crate::dom::bindings::reflector::DomGlobal;
37use crate::dom::bindings::root::{DomRoot, MutNullableDom};
38use crate::dom::bindings::str::{DOMString, USVString};
39use crate::dom::bindings::utils::to_frozen_array;
40#[cfg(feature = "bluetooth")]
41use crate::dom::bluetooth::Bluetooth;
42use crate::dom::clipboard::Clipboard;
43use crate::dom::credentialmanagement::credentialscontainer::CredentialsContainer;
44use crate::dom::csp::{GlobalCspReporting, Violation};
45#[cfg(feature = "gamepad")]
46use crate::dom::gamepad::Gamepad;
47use crate::dom::geolocation::Geolocation;
48use crate::dom::globalscope::GlobalScope;
49use crate::dom::mediadevices::MediaDevices;
50use crate::dom::mediasession::MediaSession;
51use crate::dom::mimetypearray::MimeTypeArray;
52use crate::dom::navigatorinfo;
53use crate::dom::performance::performanceresourcetiming::InitiatorType;
54use crate::dom::permissions::Permissions;
55use crate::dom::pluginarray::PluginArray;
56use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
57use crate::dom::servointernals::ServoInternals;
58use crate::dom::storagemanager::StorageManager;
59use crate::dom::types::UserActivation;
60use crate::dom::wakelock::WakeLock;
61#[cfg(feature = "webgpu")]
62use crate::dom::webgpu::gpu::GPU;
63use crate::dom::window::Window;
64#[cfg(feature = "webxr")]
65use crate::dom::xrsystem::XRSystem;
66use crate::fetch::RequestWithGlobalScope;
67use crate::network_listener::{FetchResponseListener, ResourceTimingListener, submit_timing};
68use crate::script_runtime::CanGc;
69
70pub(super) fn hardware_concurrency() -> u64 {
71 static CPUS: LazyLock<u64> = LazyLock::new(|| num_cpus::get().try_into().unwrap_or(1));
72
73 *CPUS
74}
75
76static SAFELISTED_SCHEMES: [&str; 24] = [
78 "bitcoin",
79 "ftp",
80 "ftps",
81 "geo",
82 "im",
83 "irc",
84 "ircs",
85 "magnet",
86 "mailto",
87 "matrix",
88 "mms",
89 "news",
90 "nntp",
91 "openpgp4fpr",
92 "sftp",
93 "sip",
94 "sms",
95 "smsto",
96 "ssh",
97 "tel",
98 "urn",
99 "webcal",
100 "wtai",
101 "xmpp",
102];
103
104fn matches_web_plus_protocol(scheme: &str) -> bool {
106 static WEB_PLUS_SCHEME_GRAMMAR: LazyLock<Regex> =
107 LazyLock::new(|| Regex::new(r#"^web\+[a-z]+$"#).unwrap());
108
109 WEB_PLUS_SCHEME_GRAMMAR.is_match(scheme)
110}
111
112#[dom_struct]
113pub(crate) struct Navigator {
114 reflector_: Reflector,
115 #[cfg(feature = "bluetooth")]
116 bluetooth: MutNullableDom<Bluetooth>,
117 credentials: MutNullableDom<CredentialsContainer>,
118 plugins: MutNullableDom<PluginArray>,
119 mime_types: MutNullableDom<MimeTypeArray>,
120 service_worker: MutNullableDom<ServiceWorkerContainer>,
121 #[cfg(feature = "webxr")]
122 xr: MutNullableDom<XRSystem>,
123 mediadevices: MutNullableDom<MediaDevices>,
124 #[cfg(feature = "gamepad")]
126 gamepads: DomRefCell<Vec<MutNullableDom<Gamepad>>>,
127 permissions: MutNullableDom<Permissions>,
128 mediasession: MutNullableDom<MediaSession>,
129 clipboard: MutNullableDom<Clipboard>,
130 storage: MutNullableDom<StorageManager>,
131 #[cfg(feature = "webgpu")]
132 gpu: MutNullableDom<GPU>,
133 #[cfg(feature = "gamepad")]
135 has_gamepad_gesture: Cell<bool>,
136 servo_internals: MutNullableDom<ServoInternals>,
137 user_activation: MutNullableDom<UserActivation>,
138 wake_lock: MutNullableDom<WakeLock>,
139}
140
141impl Navigator {
142 fn new_inherited() -> Navigator {
143 Navigator {
144 reflector_: Reflector::new(),
145 #[cfg(feature = "bluetooth")]
146 bluetooth: Default::default(),
147 credentials: Default::default(),
148 plugins: Default::default(),
149 mime_types: Default::default(),
150 service_worker: Default::default(),
151 #[cfg(feature = "webxr")]
152 xr: Default::default(),
153 mediadevices: Default::default(),
154 #[cfg(feature = "gamepad")]
155 gamepads: Default::default(),
156 permissions: Default::default(),
157 mediasession: Default::default(),
158 clipboard: Default::default(),
159 storage: Default::default(),
160 #[cfg(feature = "webgpu")]
161 gpu: Default::default(),
162 #[cfg(feature = "gamepad")]
163 has_gamepad_gesture: Cell::new(false),
164 servo_internals: Default::default(),
165 user_activation: Default::default(),
166 wake_lock: Default::default(),
167 }
168 }
169
170 pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<Navigator> {
171 reflect_dom_object(Box::new(Navigator::new_inherited()), window, can_gc)
172 }
173
174 #[cfg(feature = "webxr")]
175 pub(crate) fn xr(&self) -> Option<DomRoot<XRSystem>> {
176 self.xr.get()
177 }
178
179 #[cfg(feature = "gamepad")]
180 pub(crate) fn get_gamepad(&self, index: usize) -> Option<DomRoot<Gamepad>> {
181 self.gamepads.borrow().get(index).and_then(|g| g.get())
182 }
183
184 #[cfg(feature = "gamepad")]
185 pub(crate) fn set_gamepad(&self, index: usize, gamepad: Option<&Gamepad>) {
186 if let Some(gamepad_to_set) = self.gamepads.borrow().get(index) {
187 gamepad_to_set.set(gamepad);
188 }
189 }
190
191 #[cfg(feature = "gamepad")]
193 pub(crate) fn select_gamepad_index(&self) -> u32 {
194 let mut gamepad_list = self.gamepads.borrow_mut();
195 if let Some(index) = gamepad_list.iter().position(|g| g.get().is_none()) {
196 index as u32
197 } else {
198 let len = gamepad_list.len();
199 gamepad_list.resize_with(len + 1, Default::default);
200 len as u32
201 }
202 }
203
204 #[cfg(feature = "gamepad")]
206 pub(crate) fn shrink_gamepads_list(&self) {
207 let mut gamepad_list = self.gamepads.borrow_mut();
208 for i in (0..gamepad_list.len()).rev() {
209 if gamepad_list.get(i).is_none() {
210 gamepad_list.remove(i);
211 } else {
212 break;
213 }
214 }
215 }
216
217 #[cfg(feature = "gamepad")]
218 pub(crate) fn get_connected_gamepad(&self) -> Vec<DomRoot<Gamepad>> {
219 self.gamepads
220 .borrow()
221 .iter()
222 .filter_map(|gamepad| gamepad.get())
223 .filter(|gamepad| gamepad.connected())
224 .collect()
225 }
226
227 #[cfg(feature = "gamepad")]
228 pub(crate) fn has_gamepad_gesture(&self) -> bool {
229 self.has_gamepad_gesture.get()
230 }
231
232 #[cfg(feature = "gamepad")]
233 pub(crate) fn set_has_gamepad_gesture(&self, has_gamepad_gesture: bool) {
234 self.has_gamepad_gesture.set(has_gamepad_gesture);
235 }
236
237 fn normalize_protocol_handler_parameters(
239 &self,
240 scheme: DOMString,
241 url: USVString,
242 ) -> Fallible<(String, ServoUrl)> {
243 let scheme = scheme.to_ascii_lowercase();
245 if !SAFELISTED_SCHEMES.contains(&scheme.as_ref()) && !matches_web_plus_protocol(&scheme) {
248 return Err(Error::Security(None));
249 }
250 if !url.contains("%s") {
252 return Err(Error::Syntax(Some(
253 "Missing replacement string %s in URL".to_owned(),
254 )));
255 }
256 let environment = self.global();
258 let window = environment.as_window();
260 let Ok(url) = window.Document().encoding_parse_a_url(&url) else {
261 return Err(Error::Syntax(Some("Cannot parse URL".to_owned())));
263 };
264 if !matches!(url.scheme(), "http" | "https") {
267 return Err(Error::Security(None));
268 }
269 let environment_origin = environment.origin().immutable().clone();
270 if url.origin() != environment_origin {
271 return Err(Error::Security(None));
272 }
273 assert!(url.is_potentially_trustworthy());
275 Ok((scheme, url))
277 }
278
279 fn send_protocol_update_registration_to_embedder(
280 &self,
281 registration: ProtocolHandlerUpdateRegistration,
282 ) {
283 let global = self.global();
284 let window = global.as_window();
285 let (sender, _) = generic_channel::channel().unwrap();
286 let _ = global
287 .script_to_embedder_chan()
288 .send(EmbedderMsg::AllowProtocolHandlerRequest(
289 window.webview_id(),
290 registration,
291 sender,
292 ));
293 }
294}
295
296impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
297 fn Product(&self) -> DOMString {
299 navigatorinfo::Product()
300 }
301
302 fn ProductSub(&self) -> DOMString {
304 navigatorinfo::ProductSub()
305 }
306
307 fn Vendor(&self) -> DOMString {
309 navigatorinfo::Vendor()
310 }
311
312 fn VendorSub(&self) -> DOMString {
314 navigatorinfo::VendorSub()
315 }
316
317 fn TaintEnabled(&self) -> bool {
319 navigatorinfo::TaintEnabled()
320 }
321
322 fn AppName(&self) -> DOMString {
324 navigatorinfo::AppName()
325 }
326
327 fn AppCodeName(&self) -> DOMString {
329 navigatorinfo::AppCodeName()
330 }
331
332 fn Platform(&self) -> DOMString {
334 navigatorinfo::Platform()
335 }
336
337 fn UserAgent(&self) -> DOMString {
339 navigatorinfo::UserAgent(&pref!(user_agent))
340 }
341
342 fn AppVersion(&self) -> DOMString {
344 navigatorinfo::AppVersion()
345 }
346
347 #[cfg(feature = "bluetooth")]
349 fn Bluetooth(&self, cx: &mut js::context::JSContext) -> DomRoot<Bluetooth> {
350 self.bluetooth
351 .or_init(|| Bluetooth::new(cx, &self.global()))
352 }
353
354 fn Credentials(&self) -> DomRoot<CredentialsContainer> {
356 self.credentials
357 .or_init(|| CredentialsContainer::new(&self.global(), CanGc::deprecated_note()))
358 }
359
360 fn Geolocation(&self) -> DomRoot<Geolocation> {
362 Geolocation::new(&self.global(), CanGc::deprecated_note())
363 }
364
365 fn Language(&self) -> DOMString {
367 navigatorinfo::Language()
368 }
369
370 fn Languages(&self, cx: &mut js::context::JSContext, retval: MutableHandleValue) {
372 to_frozen_array(&[self.Language()], cx.into(), retval, CanGc::from_cx(cx))
373 }
374
375 fn OnLine(&self) -> bool {
377 true
378 }
379
380 fn Plugins(&self) -> DomRoot<PluginArray> {
382 self.plugins
383 .or_init(|| PluginArray::new(&self.global(), CanGc::deprecated_note()))
384 }
385
386 fn MimeTypes(&self) -> DomRoot<MimeTypeArray> {
388 self.mime_types
389 .or_init(|| MimeTypeArray::new(&self.global(), CanGc::deprecated_note()))
390 }
391
392 fn JavaEnabled(&self) -> bool {
394 false
395 }
396
397 fn PdfViewerEnabled(&self) -> bool {
399 false
400 }
401
402 fn ServiceWorker(&self, cx: &mut js::context::JSContext) -> DomRoot<ServiceWorkerContainer> {
404 self.service_worker
405 .or_init(|| ServiceWorkerContainer::new(cx, &self.global()))
406 }
407
408 fn CookieEnabled(&self) -> bool {
410 true
411 }
412
413 #[cfg(feature = "gamepad")]
415 fn GetGamepads(&self) -> Vec<Option<DomRoot<Gamepad>>> {
416 use script_bindings::codegen::GenericBindings::PerformanceBinding::PerformanceMethods;
417
418 let global = self.global();
420 let window = global.as_window();
421 let doc = window.Document();
422
423 if !doc.is_fully_active() || !self.has_gamepad_gesture.get() {
428 return Vec::new();
429 }
430
431 let now = *window.Performance().Now();
433
434 self.gamepads
437 .borrow()
438 .iter()
439 .map(|slot| {
440 slot.get().inspect(|gamepad| {
441 if !gamepad.exposed() {
443 gamepad.set_exposed(true);
445 gamepad.update_timestamp(now);
447 }
448 })
449 })
450 .collect() }
453 fn Permissions(&self) -> DomRoot<Permissions> {
455 self.permissions
456 .or_init(|| Permissions::new(&self.global(), CanGc::deprecated_note()))
457 }
458
459 #[cfg(feature = "webxr")]
461 fn Xr(&self) -> DomRoot<XRSystem> {
462 self.xr
463 .or_init(|| XRSystem::new(self.global().as_window(), CanGc::deprecated_note()))
464 }
465
466 fn MediaDevices(&self) -> DomRoot<MediaDevices> {
468 self.mediadevices
469 .or_init(|| MediaDevices::new(&self.global(), CanGc::deprecated_note()))
470 }
471
472 fn MediaSession(&self) -> DomRoot<MediaSession> {
474 self.mediasession.or_init(|| {
475 let global = self.global();
483 let window = global.as_window();
484 MediaSession::new(window, CanGc::deprecated_note())
485 })
486 }
487
488 #[cfg(feature = "webgpu")]
490 fn Gpu(&self) -> DomRoot<GPU> {
491 self.gpu
492 .or_init(|| GPU::new(&self.global(), CanGc::deprecated_note()))
493 }
494
495 fn HardwareConcurrency(&self) -> u64 {
497 hardware_concurrency()
498 }
499
500 fn Clipboard(&self, cx: &mut js::context::JSContext) -> DomRoot<Clipboard> {
502 self.clipboard
503 .or_init(|| Clipboard::new(cx, &self.global()))
504 }
505
506 fn Storage(&self, cx: &mut js::context::JSContext) -> DomRoot<StorageManager> {
508 self.storage
509 .or_init(|| StorageManager::new(&self.global(), CanGc::from_cx(cx)))
510 }
511
512 fn SendBeacon(
514 &self,
515 cx: &mut js::context::JSContext,
516 url: USVString,
517 data: Option<BodyInit>,
518 ) -> Fallible<bool> {
519 let global = self.global();
520 let base = global.api_base_url();
522 let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
530 return Err(Error::Type(c"Cannot parse URL".to_owned()));
531 };
532 if !matches!(url.scheme(), "http" | "https") {
533 return Err(Error::Type(c"URL is not http(s)".to_owned()));
534 }
535 let mut request_body = None;
536 let mut headers = HeaderMap::with_capacity(1);
538 let mut cors_mode = RequestMode::NoCors;
540 if let Some(data) = data {
542 let extracted_body = data.extract(cx, &global, true)?;
545 if let Some(total_bytes) = extracted_body.total_bytes {
549 let in_flight_keep_alive_bytes =
550 global.total_size_of_in_flight_keep_alive_records();
551 if total_bytes as u64 + in_flight_keep_alive_bytes > 64 * 1024 {
552 return Ok(false);
553 }
554 }
555 if let Some(content_type) = extracted_body.content_type.as_ref() {
557 cors_mode = RequestMode::CorsMode;
559 if is_cors_safelisted_request_content_type(content_type.as_bytes().deref()) {
562 cors_mode = RequestMode::NoCors;
563 }
564 headers.insert(
569 header::CONTENT_TYPE,
570 HeaderValue::from_str(&content_type.str()).unwrap(),
571 );
572 }
573 request_body = Some(extracted_body.into_net_request_body().0);
574 }
575 let request = RequestBuilder::new(
577 None,
578 UrlWithBlobClaim::from_url_without_having_claimed_blob(url.clone()),
579 global.get_referrer(),
580 )
581 .mode(cors_mode)
582 .destination(Destination::None)
583 .with_global_scope(&global)
584 .method(http::Method::POST)
585 .body(request_body)
586 .keep_alive(true)
587 .credentials_mode(CredentialsMode::Include)
588 .headers(headers);
589 global.fetch(
591 request,
592 BeaconFetchListener {
593 url,
594 global: Trusted::new(&global),
595 },
596 global.task_manager().networking_task_source().into(),
597 );
598 Ok(true)
601 }
602
603 fn Servo(&self) -> DomRoot<ServoInternals> {
605 self.servo_internals
606 .or_init(|| ServoInternals::new(&self.global(), CanGc::deprecated_note()))
607 }
608
609 fn RegisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
611 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
614 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
626 scheme,
627 url,
628 register_or_unregister: RegisterOrUnregister::Register,
629 });
630 Ok(())
631 }
632
633 fn UnregisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
635 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
638 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
640 scheme,
641 url,
642 register_or_unregister: RegisterOrUnregister::Unregister,
643 });
644 Ok(())
645 }
646
647 fn UserActivation(&self, cx: &mut js::context::JSContext) -> DomRoot<UserActivation> {
649 self.user_activation
650 .or_init(|| UserActivation::new(cx, &self.global()))
651 }
652
653 fn WakeLock(&self, cx: &mut js::context::JSContext) -> DomRoot<WakeLock> {
655 self.wake_lock.or_init(|| WakeLock::new(cx, &self.global()))
656 }
657}
658
659struct BeaconFetchListener {
660 url: ServoUrl,
662 global: Trusted<GlobalScope>,
664}
665
666impl FetchResponseListener for BeaconFetchListener {
667 fn process_request_body(&mut self, _: RequestId) {}
668
669 fn process_response(
670 &mut self,
671 _: &mut js::context::JSContext,
672 _: RequestId,
673 fetch_metadata: Result<FetchMetadata, NetworkError>,
674 ) {
675 _ = fetch_metadata;
676 }
677
678 fn process_response_chunk(
679 &mut self,
680 _: &mut js::context::JSContext,
681 _: RequestId,
682 chunk: Vec<u8>,
683 ) {
684 _ = chunk;
685 }
686
687 fn process_response_eof(
688 self,
689 cx: &mut js::context::JSContext,
690 _: RequestId,
691 response: Result<(), NetworkError>,
692 timing: ResourceFetchTiming,
693 ) {
694 submit_timing(cx, &self, &response, &timing);
695 }
696
697 fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
698 let global = self.resource_timing_global();
699 global.report_csp_violations(violations, None, None);
700 }
701}
702
703impl ResourceTimingListener for BeaconFetchListener {
704 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
705 (InitiatorType::Beacon, self.url.clone())
706 }
707
708 fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
709 self.global.root()
710 }
711}