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;
47#[cfg(feature = "gamepad")]
48use crate::dom::gamepad::gamepadevent::GamepadEventType;
49use crate::dom::geolocation::Geolocation;
50use crate::dom::globalscope::GlobalScope;
51use crate::dom::mediadevices::MediaDevices;
52use crate::dom::mediasession::MediaSession;
53use crate::dom::mimetypearray::MimeTypeArray;
54use crate::dom::navigatorinfo;
55use crate::dom::performance::performanceresourcetiming::InitiatorType;
56use crate::dom::permissions::Permissions;
57use crate::dom::pluginarray::PluginArray;
58use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
59use crate::dom::servointernals::ServoInternals;
60use crate::dom::storagemanager::StorageManager;
61use crate::dom::types::UserActivation;
62use crate::dom::wakelock::WakeLock;
63#[cfg(feature = "webgpu")]
64use crate::dom::webgpu::gpu::GPU;
65use crate::dom::window::Window;
66#[cfg(feature = "webxr")]
67use crate::dom::xrsystem::XRSystem;
68use crate::fetch::RequestWithGlobalScope;
69use crate::network_listener::{FetchResponseListener, ResourceTimingListener, submit_timing};
70use crate::script_runtime::CanGc;
71
72pub(super) 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(window: &Window, can_gc: CanGc) -> DomRoot<Navigator> {
173 reflect_dom_object(Box::new(Navigator::new_inherited()), window, can_gc)
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: &Gamepad, can_gc: CanGc) {
188 if let Some(gamepad_to_set) = self.gamepads.borrow().get(index) {
189 gamepad_to_set.set(Some(gamepad));
190 }
191 if self.has_gamepad_gesture.get() {
192 gamepad.set_exposed(true);
193 if self.global().as_window().Document().is_fully_active() {
194 gamepad.notify_event(GamepadEventType::Connected, can_gc);
195 }
196 }
197 }
198
199 #[cfg(feature = "gamepad")]
200 pub(crate) fn remove_gamepad(&self, index: usize) {
201 if let Some(gamepad_to_remove) = self.gamepads.borrow_mut().get(index) {
202 gamepad_to_remove.set(None);
203 }
204 self.shrink_gamepads_list();
205 }
206
207 #[cfg(feature = "gamepad")]
209 pub(crate) fn select_gamepad_index(&self) -> u32 {
210 let mut gamepad_list = self.gamepads.borrow_mut();
211 if let Some(index) = gamepad_list.iter().position(|g| g.get().is_none()) {
212 index as u32
213 } else {
214 let len = gamepad_list.len();
215 gamepad_list.resize_with(len + 1, Default::default);
216 len as u32
217 }
218 }
219
220 #[cfg(feature = "gamepad")]
221 fn shrink_gamepads_list(&self) {
222 let mut gamepad_list = self.gamepads.borrow_mut();
223 for i in (0..gamepad_list.len()).rev() {
224 if gamepad_list.get(i).is_none() {
225 gamepad_list.remove(i);
226 } else {
227 break;
228 }
229 }
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) -> DomRoot<Bluetooth> {
355 self.bluetooth
356 .or_init(|| Bluetooth::new(&self.global(), CanGc::deprecated_note()))
357 }
358
359 fn Credentials(&self) -> DomRoot<CredentialsContainer> {
361 self.credentials
362 .or_init(|| CredentialsContainer::new(&self.global(), CanGc::deprecated_note()))
363 }
364
365 fn Geolocation(&self) -> DomRoot<Geolocation> {
367 Geolocation::new(&self.global(), CanGc::deprecated_note())
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(&[self.Language()], cx.into(), retval, CanGc::from_cx(cx))
378 }
379
380 fn OnLine(&self) -> bool {
382 true
383 }
384
385 fn Plugins(&self) -> DomRoot<PluginArray> {
387 self.plugins
388 .or_init(|| PluginArray::new(&self.global(), CanGc::deprecated_note()))
389 }
390
391 fn MimeTypes(&self) -> DomRoot<MimeTypeArray> {
393 self.mime_types
394 .or_init(|| MimeTypeArray::new(&self.global(), CanGc::deprecated_note()))
395 }
396
397 fn JavaEnabled(&self) -> bool {
399 false
400 }
401
402 fn PdfViewerEnabled(&self) -> bool {
404 false
405 }
406
407 fn ServiceWorker(&self) -> DomRoot<ServiceWorkerContainer> {
409 self.service_worker
410 .or_init(|| ServiceWorkerContainer::new(&self.global(), CanGc::deprecated_note()))
411 }
412
413 fn CookieEnabled(&self) -> bool {
415 true
416 }
417
418 #[cfg(feature = "gamepad")]
420 fn GetGamepads(&self) -> Vec<Option<DomRoot<Gamepad>>> {
421 let global = self.global();
422 let window = global.as_window();
423 let doc = window.Document();
424
425 if !doc.is_fully_active() || !self.has_gamepad_gesture.get() {
427 return Vec::new();
428 }
429
430 self.gamepads.borrow().iter().map(|g| g.get()).collect()
431 }
432 fn Permissions(&self) -> DomRoot<Permissions> {
434 self.permissions
435 .or_init(|| Permissions::new(&self.global(), CanGc::deprecated_note()))
436 }
437
438 #[cfg(feature = "webxr")]
440 fn Xr(&self) -> DomRoot<XRSystem> {
441 self.xr
442 .or_init(|| XRSystem::new(self.global().as_window(), CanGc::deprecated_note()))
443 }
444
445 fn MediaDevices(&self) -> DomRoot<MediaDevices> {
447 self.mediadevices
448 .or_init(|| MediaDevices::new(&self.global(), CanGc::deprecated_note()))
449 }
450
451 fn MediaSession(&self) -> DomRoot<MediaSession> {
453 self.mediasession.or_init(|| {
454 let global = self.global();
462 let window = global.as_window();
463 MediaSession::new(window, CanGc::deprecated_note())
464 })
465 }
466
467 #[cfg(feature = "webgpu")]
469 fn Gpu(&self) -> DomRoot<GPU> {
470 self.gpu
471 .or_init(|| GPU::new(&self.global(), CanGc::deprecated_note()))
472 }
473
474 fn HardwareConcurrency(&self) -> u64 {
476 hardware_concurrency()
477 }
478
479 fn Clipboard(&self, cx: &mut js::context::JSContext) -> DomRoot<Clipboard> {
481 self.clipboard
482 .or_init(|| Clipboard::new(cx, &self.global()))
483 }
484
485 fn Storage(&self, cx: &mut js::context::JSContext) -> DomRoot<StorageManager> {
487 self.storage
488 .or_init(|| StorageManager::new(&self.global(), CanGc::from_cx(cx)))
489 }
490
491 fn SendBeacon(
493 &self,
494 cx: &mut js::context::JSContext,
495 url: USVString,
496 data: Option<BodyInit>,
497 ) -> Fallible<bool> {
498 let global = self.global();
499 let base = global.api_base_url();
501 let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
509 return Err(Error::Type(c"Cannot parse URL".to_owned()));
510 };
511 if !matches!(url.scheme(), "http" | "https") {
512 return Err(Error::Type(c"URL is not http(s)".to_owned()));
513 }
514 let mut request_body = None;
515 let mut headers = HeaderMap::with_capacity(1);
517 let mut cors_mode = RequestMode::NoCors;
519 if let Some(data) = data {
521 let extracted_body = data.extract(cx, &global, true)?;
524 if let Some(total_bytes) = extracted_body.total_bytes {
528 let in_flight_keep_alive_bytes =
529 global.total_size_of_in_flight_keep_alive_records();
530 if total_bytes as u64 + in_flight_keep_alive_bytes > 64 * 1024 {
531 return Ok(false);
532 }
533 }
534 if let Some(content_type) = extracted_body.content_type.as_ref() {
536 cors_mode = RequestMode::CorsMode;
538 if is_cors_safelisted_request_content_type(content_type.as_bytes().deref()) {
541 cors_mode = RequestMode::NoCors;
542 }
543 headers.insert(
548 header::CONTENT_TYPE,
549 HeaderValue::from_str(&content_type.str()).unwrap(),
550 );
551 }
552 request_body = Some(extracted_body.into_net_request_body().0);
553 }
554 let request = RequestBuilder::new(
556 None,
557 UrlWithBlobClaim::from_url_without_having_claimed_blob(url.clone()),
558 global.get_referrer(),
559 )
560 .mode(cors_mode)
561 .destination(Destination::None)
562 .with_global_scope(&global)
563 .method(http::Method::POST)
564 .body(request_body)
565 .keep_alive(true)
566 .credentials_mode(CredentialsMode::Include)
567 .headers(headers);
568 global.fetch(
570 request,
571 BeaconFetchListener {
572 url,
573 global: Trusted::new(&global),
574 },
575 global.task_manager().networking_task_source().into(),
576 );
577 Ok(true)
580 }
581
582 fn Servo(&self) -> DomRoot<ServoInternals> {
584 self.servo_internals
585 .or_init(|| ServoInternals::new(&self.global(), CanGc::deprecated_note()))
586 }
587
588 fn RegisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
590 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
593 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
605 scheme,
606 url,
607 register_or_unregister: RegisterOrUnregister::Register,
608 });
609 Ok(())
610 }
611
612 fn UnregisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
614 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
617 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
619 scheme,
620 url,
621 register_or_unregister: RegisterOrUnregister::Unregister,
622 });
623 Ok(())
624 }
625
626 fn UserActivation(&self, cx: &mut js::context::JSContext) -> DomRoot<UserActivation> {
628 self.user_activation
629 .or_init(|| UserActivation::new(cx, &self.global()))
630 }
631
632 fn WakeLock(&self, cx: &mut js::context::JSContext) -> DomRoot<WakeLock> {
634 self.wake_lock.or_init(|| WakeLock::new(cx, &self.global()))
635 }
636}
637
638struct BeaconFetchListener {
639 url: ServoUrl,
641 global: Trusted<GlobalScope>,
643}
644
645impl FetchResponseListener for BeaconFetchListener {
646 fn process_request_body(&mut self, _: RequestId) {}
647
648 fn process_response(
649 &mut self,
650 _: &mut js::context::JSContext,
651 _: RequestId,
652 fetch_metadata: Result<FetchMetadata, NetworkError>,
653 ) {
654 _ = fetch_metadata;
655 }
656
657 fn process_response_chunk(
658 &mut self,
659 _: &mut js::context::JSContext,
660 _: RequestId,
661 chunk: Vec<u8>,
662 ) {
663 _ = chunk;
664 }
665
666 fn process_response_eof(
667 self,
668 cx: &mut js::context::JSContext,
669 _: RequestId,
670 response: Result<(), NetworkError>,
671 timing: ResourceFetchTiming,
672 ) {
673 submit_timing(cx, &self, &response, &timing);
674 }
675
676 fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
677 let global = self.resource_timing_global();
678 global.report_csp_violations(violations, None, None);
679 }
680}
681
682impl ResourceTimingListener for BeaconFetchListener {
683 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
684 (InitiatorType::Beacon, self.url.clone())
685 }
686
687 fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
688 self.global.root()
689 }
690}