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;
23use servo_base::generic_channel;
24use servo_config::pref;
25use servo_url::ServoUrl;
26
27use crate::body::Extractable;
28#[cfg(feature = "gamepad")]
29use crate::dom::bindings::cell::DomRefCell;
30use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods;
31use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
32use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit;
33use crate::dom::bindings::error::{Error, Fallible};
34use crate::dom::bindings::refcounted::Trusted;
35use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
36use crate::dom::bindings::root::{DomRoot, MutNullableDom};
37use crate::dom::bindings::str::{DOMString, USVString};
38use crate::dom::bindings::utils::to_frozen_array;
39#[cfg(feature = "bluetooth")]
40use crate::dom::bluetooth::Bluetooth;
41use crate::dom::clipboard::Clipboard;
42use crate::dom::credentialmanagement::credentialscontainer::CredentialsContainer;
43use crate::dom::csp::{GlobalCspReporting, Violation};
44#[cfg(feature = "gamepad")]
45use crate::dom::gamepad::Gamepad;
46#[cfg(feature = "gamepad")]
47use crate::dom::gamepad::gamepadevent::GamepadEventType;
48use crate::dom::geolocation::Geolocation;
49use crate::dom::globalscope::GlobalScope;
50use crate::dom::mediadevices::MediaDevices;
51use crate::dom::mediasession::MediaSession;
52use crate::dom::mimetypearray::MimeTypeArray;
53use crate::dom::navigatorinfo;
54use crate::dom::performance::performanceresourcetiming::InitiatorType;
55use crate::dom::permissions::Permissions;
56use crate::dom::pluginarray::PluginArray;
57use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
58use crate::dom::servointernals::ServoInternals;
59use crate::dom::types::UserActivation;
60#[cfg(feature = "webgpu")]
61use crate::dom::webgpu::gpu::GPU;
62use crate::dom::window::Window;
63#[cfg(feature = "webxr")]
64use crate::dom::xrsystem::XRSystem;
65use crate::fetch::RequestWithGlobalScope;
66use crate::network_listener::{FetchResponseListener, ResourceTimingListener, submit_timing};
67use crate::script_runtime::{CanGc, JSContext};
68
69pub(super) fn hardware_concurrency() -> u64 {
70 static CPUS: LazyLock<u64> = LazyLock::new(|| num_cpus::get().try_into().unwrap_or(1));
71
72 *CPUS
73}
74
75static SAFELISTED_SCHEMES: [&str; 24] = [
77 "bitcoin",
78 "ftp",
79 "ftps",
80 "geo",
81 "im",
82 "irc",
83 "ircs",
84 "magnet",
85 "mailto",
86 "matrix",
87 "mms",
88 "news",
89 "nntp",
90 "openpgp4fpr",
91 "sftp",
92 "sip",
93 "sms",
94 "smsto",
95 "ssh",
96 "tel",
97 "urn",
98 "webcal",
99 "wtai",
100 "xmpp",
101];
102
103fn matches_web_plus_protocol(scheme: &str) -> bool {
105 static WEB_PLUS_SCHEME_GRAMMAR: LazyLock<Regex> =
106 LazyLock::new(|| Regex::new(r#"^web\+[a-z]+$"#).unwrap());
107
108 WEB_PLUS_SCHEME_GRAMMAR.is_match(scheme)
109}
110
111#[dom_struct]
112pub(crate) struct Navigator {
113 reflector_: Reflector,
114 #[cfg(feature = "bluetooth")]
115 bluetooth: MutNullableDom<Bluetooth>,
116 credentials: MutNullableDom<CredentialsContainer>,
117 plugins: MutNullableDom<PluginArray>,
118 mime_types: MutNullableDom<MimeTypeArray>,
119 service_worker: MutNullableDom<ServiceWorkerContainer>,
120 #[cfg(feature = "webxr")]
121 xr: MutNullableDom<XRSystem>,
122 mediadevices: MutNullableDom<MediaDevices>,
123 #[cfg(feature = "gamepad")]
125 gamepads: DomRefCell<Vec<MutNullableDom<Gamepad>>>,
126 permissions: MutNullableDom<Permissions>,
127 mediasession: MutNullableDom<MediaSession>,
128 clipboard: MutNullableDom<Clipboard>,
129 #[cfg(feature = "webgpu")]
130 gpu: MutNullableDom<GPU>,
131 #[cfg(feature = "gamepad")]
133 has_gamepad_gesture: Cell<bool>,
134 servo_internals: MutNullableDom<ServoInternals>,
135 user_activation: MutNullableDom<UserActivation>,
136}
137
138impl Navigator {
139 fn new_inherited() -> Navigator {
140 Navigator {
141 reflector_: Reflector::new(),
142 #[cfg(feature = "bluetooth")]
143 bluetooth: Default::default(),
144 credentials: Default::default(),
145 plugins: Default::default(),
146 mime_types: Default::default(),
147 service_worker: Default::default(),
148 #[cfg(feature = "webxr")]
149 xr: Default::default(),
150 mediadevices: Default::default(),
151 #[cfg(feature = "gamepad")]
152 gamepads: Default::default(),
153 permissions: Default::default(),
154 mediasession: Default::default(),
155 clipboard: Default::default(),
156 #[cfg(feature = "webgpu")]
157 gpu: Default::default(),
158 #[cfg(feature = "gamepad")]
159 has_gamepad_gesture: Cell::new(false),
160 servo_internals: Default::default(),
161 user_activation: Default::default(),
162 }
163 }
164
165 pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<Navigator> {
166 reflect_dom_object(Box::new(Navigator::new_inherited()), window, can_gc)
167 }
168
169 #[cfg(feature = "webxr")]
170 pub(crate) fn xr(&self) -> Option<DomRoot<XRSystem>> {
171 self.xr.get()
172 }
173
174 #[cfg(feature = "gamepad")]
175 pub(crate) fn get_gamepad(&self, index: usize) -> Option<DomRoot<Gamepad>> {
176 self.gamepads.borrow().get(index).and_then(|g| g.get())
177 }
178
179 #[cfg(feature = "gamepad")]
180 pub(crate) fn set_gamepad(&self, index: usize, gamepad: &Gamepad, can_gc: CanGc) {
181 if let Some(gamepad_to_set) = self.gamepads.borrow().get(index) {
182 gamepad_to_set.set(Some(gamepad));
183 }
184 if self.has_gamepad_gesture.get() {
185 gamepad.set_exposed(true);
186 if self.global().as_window().Document().is_fully_active() {
187 gamepad.notify_event(GamepadEventType::Connected, can_gc);
188 }
189 }
190 }
191
192 #[cfg(feature = "gamepad")]
193 pub(crate) fn remove_gamepad(&self, index: usize) {
194 if let Some(gamepad_to_remove) = self.gamepads.borrow_mut().get(index) {
195 gamepad_to_remove.set(None);
196 }
197 self.shrink_gamepads_list();
198 }
199
200 #[cfg(feature = "gamepad")]
202 pub(crate) fn select_gamepad_index(&self) -> u32 {
203 let mut gamepad_list = self.gamepads.borrow_mut();
204 if let Some(index) = gamepad_list.iter().position(|g| g.get().is_none()) {
205 index as u32
206 } else {
207 let len = gamepad_list.len();
208 gamepad_list.resize_with(len + 1, Default::default);
209 len as u32
210 }
211 }
212
213 #[cfg(feature = "gamepad")]
214 fn shrink_gamepads_list(&self) {
215 let mut gamepad_list = self.gamepads.borrow_mut();
216 for i in (0..gamepad_list.len()).rev() {
217 if gamepad_list.get(i).is_none() {
218 gamepad_list.remove(i);
219 } else {
220 break;
221 }
222 }
223 }
224
225 #[cfg(feature = "gamepad")]
226 pub(crate) fn has_gamepad_gesture(&self) -> bool {
227 self.has_gamepad_gesture.get()
228 }
229
230 #[cfg(feature = "gamepad")]
231 pub(crate) fn set_has_gamepad_gesture(&self, has_gamepad_gesture: bool) {
232 self.has_gamepad_gesture.set(has_gamepad_gesture);
233 }
234
235 fn normalize_protocol_handler_parameters(
237 &self,
238 scheme: DOMString,
239 url: USVString,
240 ) -> Fallible<(String, ServoUrl)> {
241 let scheme = scheme.to_ascii_lowercase();
243 if !SAFELISTED_SCHEMES.contains(&scheme.as_ref()) && !matches_web_plus_protocol(&scheme) {
246 return Err(Error::Security(None));
247 }
248 if !url.contains("%s") {
250 return Err(Error::Syntax(Some(
251 "Missing replacement string %s in URL".to_owned(),
252 )));
253 }
254 let environment = self.global();
256 let window = environment.as_window();
258 let Ok(url) = window.Document().encoding_parse_a_url(&url) else {
259 return Err(Error::Syntax(Some("Cannot parse URL".to_owned())));
261 };
262 if !matches!(url.scheme(), "http" | "https") {
265 return Err(Error::Security(None));
266 }
267 let environment_origin = environment.origin().immutable().clone();
268 if url.origin() != environment_origin {
269 return Err(Error::Security(None));
270 }
271 assert!(url.is_potentially_trustworthy());
273 Ok((scheme, url))
275 }
276
277 fn send_protocol_update_registration_to_embedder(
278 &self,
279 registration: ProtocolHandlerUpdateRegistration,
280 ) {
281 let global = self.global();
282 let window = global.as_window();
283 let (sender, _) = generic_channel::channel().unwrap();
284 let _ = global
285 .script_to_embedder_chan()
286 .send(EmbedderMsg::AllowProtocolHandlerRequest(
287 window.webview_id(),
288 registration,
289 sender,
290 ));
291 }
292}
293
294impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
295 fn Product(&self) -> DOMString {
297 navigatorinfo::Product()
298 }
299
300 fn ProductSub(&self) -> DOMString {
302 navigatorinfo::ProductSub()
303 }
304
305 fn Vendor(&self) -> DOMString {
307 navigatorinfo::Vendor()
308 }
309
310 fn VendorSub(&self) -> DOMString {
312 navigatorinfo::VendorSub()
313 }
314
315 fn TaintEnabled(&self) -> bool {
317 navigatorinfo::TaintEnabled()
318 }
319
320 fn AppName(&self) -> DOMString {
322 navigatorinfo::AppName()
323 }
324
325 fn AppCodeName(&self) -> DOMString {
327 navigatorinfo::AppCodeName()
328 }
329
330 fn Platform(&self) -> DOMString {
332 navigatorinfo::Platform()
333 }
334
335 fn UserAgent(&self) -> DOMString {
337 navigatorinfo::UserAgent(&pref!(user_agent))
338 }
339
340 fn AppVersion(&self) -> DOMString {
342 navigatorinfo::AppVersion()
343 }
344
345 #[cfg(feature = "bluetooth")]
347 fn Bluetooth(&self) -> DomRoot<Bluetooth> {
348 self.bluetooth
349 .or_init(|| Bluetooth::new(&self.global(), CanGc::note()))
350 }
351
352 fn Credentials(&self) -> DomRoot<CredentialsContainer> {
354 self.credentials
355 .or_init(|| CredentialsContainer::new(&self.global(), CanGc::note()))
356 }
357
358 fn Geolocation(&self) -> DomRoot<Geolocation> {
360 Geolocation::new(&self.global(), CanGc::note())
361 }
362
363 fn Language(&self) -> DOMString {
365 navigatorinfo::Language()
366 }
367
368 fn Languages(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
370 to_frozen_array(&[self.Language()], cx, retval, can_gc)
371 }
372
373 fn OnLine(&self) -> bool {
375 true
376 }
377
378 fn Plugins(&self) -> DomRoot<PluginArray> {
380 self.plugins
381 .or_init(|| PluginArray::new(&self.global(), CanGc::note()))
382 }
383
384 fn MimeTypes(&self) -> DomRoot<MimeTypeArray> {
386 self.mime_types
387 .or_init(|| MimeTypeArray::new(&self.global(), CanGc::note()))
388 }
389
390 fn JavaEnabled(&self) -> bool {
392 false
393 }
394
395 fn PdfViewerEnabled(&self) -> bool {
397 false
398 }
399
400 fn ServiceWorker(&self) -> DomRoot<ServiceWorkerContainer> {
402 self.service_worker
403 .or_init(|| ServiceWorkerContainer::new(&self.global(), CanGc::note()))
404 }
405
406 fn CookieEnabled(&self) -> bool {
408 true
409 }
410
411 #[cfg(feature = "gamepad")]
413 fn GetGamepads(&self) -> Vec<Option<DomRoot<Gamepad>>> {
414 let global = self.global();
415 let window = global.as_window();
416 let doc = window.Document();
417
418 if !doc.is_fully_active() || !self.has_gamepad_gesture.get() {
420 return Vec::new();
421 }
422
423 self.gamepads.borrow().iter().map(|g| g.get()).collect()
424 }
425 fn Permissions(&self) -> DomRoot<Permissions> {
427 self.permissions
428 .or_init(|| Permissions::new(&self.global(), CanGc::note()))
429 }
430
431 #[cfg(feature = "webxr")]
433 fn Xr(&self) -> DomRoot<XRSystem> {
434 self.xr
435 .or_init(|| XRSystem::new(self.global().as_window(), CanGc::note()))
436 }
437
438 fn MediaDevices(&self) -> DomRoot<MediaDevices> {
440 self.mediadevices
441 .or_init(|| MediaDevices::new(&self.global(), CanGc::note()))
442 }
443
444 fn MediaSession(&self) -> DomRoot<MediaSession> {
446 self.mediasession.or_init(|| {
447 let global = self.global();
455 let window = global.as_window();
456 MediaSession::new(window, CanGc::note())
457 })
458 }
459
460 #[cfg(feature = "webgpu")]
462 fn Gpu(&self) -> DomRoot<GPU> {
463 self.gpu.or_init(|| GPU::new(&self.global(), CanGc::note()))
464 }
465
466 fn HardwareConcurrency(&self) -> u64 {
468 hardware_concurrency()
469 }
470
471 fn Clipboard(&self, cx: &mut js::context::JSContext) -> DomRoot<Clipboard> {
473 self.clipboard
474 .or_init(|| Clipboard::new(cx, &self.global()))
475 }
476
477 fn SendBeacon(&self, url: USVString, data: Option<BodyInit>, can_gc: CanGc) -> Fallible<bool> {
479 let global = self.global();
480 let base = global.api_base_url();
482 let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
490 return Err(Error::Type(c"Cannot parse URL".to_owned()));
491 };
492 if !matches!(url.scheme(), "http" | "https") {
493 return Err(Error::Type(c"URL is not http(s)".to_owned()));
494 }
495 let mut request_body = None;
496 let mut headers = HeaderMap::with_capacity(1);
498 let mut cors_mode = RequestMode::NoCors;
500 if let Some(data) = data {
502 let extracted_body = data.extract(&global, true, can_gc)?;
505 if let Some(total_bytes) = extracted_body.total_bytes {
509 let in_flight_keep_alive_bytes =
510 global.total_size_of_in_flight_keep_alive_records();
511 if total_bytes as u64 + in_flight_keep_alive_bytes > 64 * 1024 {
512 return Ok(false);
513 }
514 }
515 if let Some(content_type) = extracted_body.content_type.as_ref() {
517 cors_mode = RequestMode::CorsMode;
519 if is_cors_safelisted_request_content_type(content_type.as_bytes().deref()) {
522 cors_mode = RequestMode::NoCors;
523 }
524 headers.insert(
529 header::CONTENT_TYPE,
530 HeaderValue::from_str(&content_type.str()).unwrap(),
531 );
532 }
533 request_body = Some(extracted_body.into_net_request_body().0);
534 }
535 let request = RequestBuilder::new(
537 None,
538 UrlWithBlobClaim::from_url_without_having_claimed_blob(url.clone()),
539 global.get_referrer(),
540 )
541 .mode(cors_mode)
542 .destination(Destination::None)
543 .with_global_scope(&global)
544 .method(http::Method::POST)
545 .body(request_body)
546 .keep_alive(true)
547 .credentials_mode(CredentialsMode::Include)
548 .headers(headers);
549 global.fetch(
551 request,
552 BeaconFetchListener {
553 url,
554 global: Trusted::new(&global),
555 },
556 global.task_manager().networking_task_source().into(),
557 );
558 Ok(true)
561 }
562
563 fn Servo(&self) -> DomRoot<ServoInternals> {
565 self.servo_internals
566 .or_init(|| ServoInternals::new(&self.global(), CanGc::note()))
567 }
568
569 fn RegisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
571 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
574 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
586 scheme,
587 url,
588 register_or_unregister: RegisterOrUnregister::Register,
589 });
590 Ok(())
591 }
592
593 fn UnregisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
595 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
598 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
600 scheme,
601 url,
602 register_or_unregister: RegisterOrUnregister::Unregister,
603 });
604 Ok(())
605 }
606
607 fn UserActivation(&self, can_gc: CanGc) -> DomRoot<UserActivation> {
609 self.user_activation
610 .or_init(|| UserActivation::new(&self.global(), can_gc))
611 }
612}
613
614struct BeaconFetchListener {
615 url: ServoUrl,
617 global: Trusted<GlobalScope>,
619}
620
621impl FetchResponseListener for BeaconFetchListener {
622 fn process_request_body(&mut self, _: RequestId) {}
623
624 fn process_response(
625 &mut self,
626 _: &mut js::context::JSContext,
627 _: RequestId,
628 fetch_metadata: Result<FetchMetadata, NetworkError>,
629 ) {
630 _ = fetch_metadata;
631 }
632
633 fn process_response_chunk(
634 &mut self,
635 _: &mut js::context::JSContext,
636 _: RequestId,
637 chunk: Vec<u8>,
638 ) {
639 _ = chunk;
640 }
641
642 fn process_response_eof(
643 self,
644 cx: &mut js::context::JSContext,
645 _: RequestId,
646 response: Result<(), NetworkError>,
647 timing: ResourceFetchTiming,
648 ) {
649 submit_timing(cx, &self, &response, &timing);
650 }
651
652 fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
653 let global = self.resource_timing_global();
654 global.report_csp_violations(violations, None, None);
655 }
656}
657
658impl ResourceTimingListener for BeaconFetchListener {
659 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
660 (InitiatorType::Beacon, self.url.clone())
661 }
662
663 fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
664 self.global.root()
665 }
666}