script/dom/
navigator.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::convert::TryInto;
7use std::ops::Deref;
8use std::sync::{Arc, LazyLock, Mutex};
9
10use dom_struct::dom_struct;
11use headers::HeaderMap;
12use http::header::{self, HeaderValue};
13use js::rust::MutableHandleValue;
14use net_traits::request::{
15    CredentialsMode, Destination, RequestBuilder, RequestId, RequestMode,
16    is_cors_safelisted_request_content_type,
17};
18use net_traits::{
19    FetchMetadata, FetchResponseListener, NetworkError, ResourceFetchTiming, ResourceTimingType,
20};
21use servo_config::pref;
22use servo_url::ServoUrl;
23
24use crate::body::Extractable;
25use crate::dom::bindings::cell::DomRefCell;
26use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods;
27use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
28use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit;
29use crate::dom::bindings::error::{Error, Fallible};
30use crate::dom::bindings::refcounted::Trusted;
31use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
32use crate::dom::bindings::root::{DomRoot, MutNullableDom};
33use crate::dom::bindings::str::{DOMString, USVString};
34use crate::dom::bindings::utils::to_frozen_array;
35#[cfg(feature = "bluetooth")]
36use crate::dom::bluetooth::Bluetooth;
37use crate::dom::clipboard::Clipboard;
38use crate::dom::credentialmanagement::credentialscontainer::CredentialsContainer;
39use crate::dom::csp::{GlobalCspReporting, Violation};
40use crate::dom::gamepad::Gamepad;
41use crate::dom::gamepad::gamepadevent::GamepadEventType;
42use crate::dom::geolocation::Geolocation;
43use crate::dom::globalscope::GlobalScope;
44use crate::dom::mediadevices::MediaDevices;
45use crate::dom::mediasession::MediaSession;
46use crate::dom::mimetypearray::MimeTypeArray;
47use crate::dom::navigatorinfo;
48use crate::dom::performanceresourcetiming::InitiatorType;
49use crate::dom::permissions::Permissions;
50use crate::dom::pluginarray::PluginArray;
51use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
52use crate::dom::servointernals::ServoInternals;
53#[cfg(feature = "webgpu")]
54use crate::dom::webgpu::gpu::GPU;
55use crate::dom::window::Window;
56#[cfg(feature = "webxr")]
57use crate::dom::xrsystem::XRSystem;
58use crate::network_listener::{PreInvoke, ResourceTimingListener, submit_timing};
59use crate::script_runtime::{CanGc, JSContext};
60
61pub(super) fn hardware_concurrency() -> u64 {
62    static CPUS: LazyLock<u64> = LazyLock::new(|| num_cpus::get().try_into().unwrap_or(1));
63
64    *CPUS
65}
66
67#[dom_struct]
68pub(crate) struct Navigator {
69    reflector_: Reflector,
70    #[cfg(feature = "bluetooth")]
71    bluetooth: MutNullableDom<Bluetooth>,
72    credentials: MutNullableDom<CredentialsContainer>,
73    plugins: MutNullableDom<PluginArray>,
74    mime_types: MutNullableDom<MimeTypeArray>,
75    service_worker: MutNullableDom<ServiceWorkerContainer>,
76    #[cfg(feature = "webxr")]
77    xr: MutNullableDom<XRSystem>,
78    mediadevices: MutNullableDom<MediaDevices>,
79    /// <https://www.w3.org/TR/gamepad/#dfn-gamepads>
80    gamepads: DomRefCell<Vec<MutNullableDom<Gamepad>>>,
81    permissions: MutNullableDom<Permissions>,
82    mediasession: MutNullableDom<MediaSession>,
83    clipboard: MutNullableDom<Clipboard>,
84    #[cfg(feature = "webgpu")]
85    gpu: MutNullableDom<GPU>,
86    /// <https://www.w3.org/TR/gamepad/#dfn-hasgamepadgesture>
87    has_gamepad_gesture: Cell<bool>,
88    servo_internals: MutNullableDom<ServoInternals>,
89}
90
91impl Navigator {
92    fn new_inherited() -> Navigator {
93        Navigator {
94            reflector_: Reflector::new(),
95            #[cfg(feature = "bluetooth")]
96            bluetooth: Default::default(),
97            credentials: Default::default(),
98            plugins: Default::default(),
99            mime_types: Default::default(),
100            service_worker: Default::default(),
101            #[cfg(feature = "webxr")]
102            xr: Default::default(),
103            mediadevices: Default::default(),
104            gamepads: Default::default(),
105            permissions: Default::default(),
106            mediasession: Default::default(),
107            clipboard: Default::default(),
108            #[cfg(feature = "webgpu")]
109            gpu: Default::default(),
110            has_gamepad_gesture: Cell::new(false),
111            servo_internals: Default::default(),
112        }
113    }
114
115    pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<Navigator> {
116        reflect_dom_object(Box::new(Navigator::new_inherited()), window, can_gc)
117    }
118
119    #[cfg(feature = "webxr")]
120    pub(crate) fn xr(&self) -> Option<DomRoot<XRSystem>> {
121        self.xr.get()
122    }
123
124    pub(crate) fn get_gamepad(&self, index: usize) -> Option<DomRoot<Gamepad>> {
125        self.gamepads.borrow().get(index).and_then(|g| g.get())
126    }
127
128    pub(crate) fn set_gamepad(&self, index: usize, gamepad: &Gamepad, can_gc: CanGc) {
129        if let Some(gamepad_to_set) = self.gamepads.borrow().get(index) {
130            gamepad_to_set.set(Some(gamepad));
131        }
132        if self.has_gamepad_gesture.get() {
133            gamepad.set_exposed(true);
134            if self.global().as_window().Document().is_fully_active() {
135                gamepad.notify_event(GamepadEventType::Connected, can_gc);
136            }
137        }
138    }
139
140    pub(crate) fn remove_gamepad(&self, index: usize) {
141        if let Some(gamepad_to_remove) = self.gamepads.borrow_mut().get(index) {
142            gamepad_to_remove.set(None);
143        }
144        self.shrink_gamepads_list();
145    }
146
147    /// <https://www.w3.org/TR/gamepad/#dfn-selecting-an-unused-gamepad-index>
148    pub(crate) fn select_gamepad_index(&self) -> u32 {
149        let mut gamepad_list = self.gamepads.borrow_mut();
150        if let Some(index) = gamepad_list.iter().position(|g| g.get().is_none()) {
151            index as u32
152        } else {
153            let len = gamepad_list.len();
154            gamepad_list.resize_with(len + 1, Default::default);
155            len as u32
156        }
157    }
158
159    fn shrink_gamepads_list(&self) {
160        let mut gamepad_list = self.gamepads.borrow_mut();
161        for i in (0..gamepad_list.len()).rev() {
162            if gamepad_list.get(i).is_none() {
163                gamepad_list.remove(i);
164            } else {
165                break;
166            }
167        }
168    }
169
170    pub(crate) fn has_gamepad_gesture(&self) -> bool {
171        self.has_gamepad_gesture.get()
172    }
173
174    pub(crate) fn set_has_gamepad_gesture(&self, has_gamepad_gesture: bool) {
175        self.has_gamepad_gesture.set(has_gamepad_gesture);
176    }
177}
178
179impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
180    // https://html.spec.whatwg.org/multipage/#dom-navigator-product
181    fn Product(&self) -> DOMString {
182        navigatorinfo::Product()
183    }
184
185    // https://html.spec.whatwg.org/multipage/#dom-navigator-productsub
186    fn ProductSub(&self) -> DOMString {
187        navigatorinfo::ProductSub()
188    }
189
190    // https://html.spec.whatwg.org/multipage/#dom-navigator-vendor
191    fn Vendor(&self) -> DOMString {
192        navigatorinfo::Vendor()
193    }
194
195    // https://html.spec.whatwg.org/multipage/#dom-navigator-vendorsub
196    fn VendorSub(&self) -> DOMString {
197        navigatorinfo::VendorSub()
198    }
199
200    // https://html.spec.whatwg.org/multipage/#dom-navigator-taintenabled
201    fn TaintEnabled(&self) -> bool {
202        navigatorinfo::TaintEnabled()
203    }
204
205    // https://html.spec.whatwg.org/multipage/#dom-navigator-appname
206    fn AppName(&self) -> DOMString {
207        navigatorinfo::AppName()
208    }
209
210    // https://html.spec.whatwg.org/multipage/#dom-navigator-appcodename
211    fn AppCodeName(&self) -> DOMString {
212        navigatorinfo::AppCodeName()
213    }
214
215    // https://html.spec.whatwg.org/multipage/#dom-navigator-platform
216    fn Platform(&self) -> DOMString {
217        navigatorinfo::Platform()
218    }
219
220    // https://html.spec.whatwg.org/multipage/#dom-navigator-useragent
221    fn UserAgent(&self) -> DOMString {
222        navigatorinfo::UserAgent(&pref!(user_agent))
223    }
224
225    // https://html.spec.whatwg.org/multipage/#dom-navigator-appversion
226    fn AppVersion(&self) -> DOMString {
227        navigatorinfo::AppVersion()
228    }
229
230    // https://webbluetoothcg.github.io/web-bluetooth/#dom-navigator-bluetooth
231    #[cfg(feature = "bluetooth")]
232    fn Bluetooth(&self) -> DomRoot<Bluetooth> {
233        self.bluetooth
234            .or_init(|| Bluetooth::new(&self.global(), CanGc::note()))
235    }
236
237    // https://www.w3.org/TR/credential-management-1/#framework-credential-management
238    fn Credentials(&self) -> DomRoot<CredentialsContainer> {
239        self.credentials
240            .or_init(|| CredentialsContainer::new(&self.global(), CanGc::note()))
241    }
242
243    // https://www.w3.org/TR/geolocation/#navigator_interface
244    fn Geolocation(&self) -> DomRoot<Geolocation> {
245        Geolocation::new(&self.global(), CanGc::note())
246    }
247
248    // https://html.spec.whatwg.org/multipage/#navigatorlanguage
249    fn Language(&self) -> DOMString {
250        navigatorinfo::Language()
251    }
252
253    // https://html.spec.whatwg.org/multipage/#dom-navigator-languages
254    #[allow(unsafe_code)]
255    fn Languages(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
256        to_frozen_array(&[self.Language()], cx, retval, can_gc)
257    }
258
259    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-online>
260    fn OnLine(&self) -> bool {
261        true
262    }
263
264    // https://html.spec.whatwg.org/multipage/#dom-navigator-plugins
265    fn Plugins(&self) -> DomRoot<PluginArray> {
266        self.plugins
267            .or_init(|| PluginArray::new(&self.global(), CanGc::note()))
268    }
269
270    // https://html.spec.whatwg.org/multipage/#dom-navigator-mimetypes
271    fn MimeTypes(&self) -> DomRoot<MimeTypeArray> {
272        self.mime_types
273            .or_init(|| MimeTypeArray::new(&self.global(), CanGc::note()))
274    }
275
276    // https://html.spec.whatwg.org/multipage/#dom-navigator-javaenabled
277    fn JavaEnabled(&self) -> bool {
278        false
279    }
280
281    // https://w3c.github.io/ServiceWorker/#navigator-service-worker-attribute
282    fn ServiceWorker(&self) -> DomRoot<ServiceWorkerContainer> {
283        self.service_worker
284            .or_init(|| ServiceWorkerContainer::new(&self.global(), CanGc::note()))
285    }
286
287    // https://html.spec.whatwg.org/multipage/#dom-navigator-cookieenabled
288    fn CookieEnabled(&self) -> bool {
289        true
290    }
291
292    /// <https://www.w3.org/TR/gamepad/#dom-navigator-getgamepads>
293    fn GetGamepads(&self) -> Vec<Option<DomRoot<Gamepad>>> {
294        let global = self.global();
295        let window = global.as_window();
296        let doc = window.Document();
297
298        // TODO: Handle permissions policy once implemented
299        if !doc.is_fully_active() || !self.has_gamepad_gesture.get() {
300            return Vec::new();
301        }
302
303        self.gamepads.borrow().iter().map(|g| g.get()).collect()
304    }
305    // https://w3c.github.io/permissions/#navigator-and-workernavigator-extension
306    fn Permissions(&self) -> DomRoot<Permissions> {
307        self.permissions
308            .or_init(|| Permissions::new(&self.global(), CanGc::note()))
309    }
310
311    /// <https://immersive-web.github.io/webxr/#dom-navigator-xr>
312    #[cfg(feature = "webxr")]
313    fn Xr(&self) -> DomRoot<XRSystem> {
314        self.xr
315            .or_init(|| XRSystem::new(self.global().as_window(), CanGc::note()))
316    }
317
318    /// <https://w3c.github.io/mediacapture-main/#dom-navigator-mediadevices>
319    fn MediaDevices(&self) -> DomRoot<MediaDevices> {
320        self.mediadevices
321            .or_init(|| MediaDevices::new(&self.global(), CanGc::note()))
322    }
323
324    /// <https://w3c.github.io/mediasession/#dom-navigator-mediasession>
325    fn MediaSession(&self) -> DomRoot<MediaSession> {
326        self.mediasession.or_init(|| {
327            // There is a single MediaSession instance per Pipeline
328            // and only one active MediaSession globally.
329            //
330            // MediaSession creation can happen in two cases:
331            //
332            // - If content gets `navigator.mediaSession`
333            // - If a media instance (HTMLMediaElement so far) starts playing media.
334            let global = self.global();
335            let window = global.as_window();
336            MediaSession::new(window, CanGc::note())
337        })
338    }
339
340    // https://gpuweb.github.io/gpuweb/#dom-navigator-gpu
341    #[cfg(feature = "webgpu")]
342    fn Gpu(&self) -> DomRoot<GPU> {
343        self.gpu.or_init(|| GPU::new(&self.global(), CanGc::note()))
344    }
345
346    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-hardwareconcurrency>
347    fn HardwareConcurrency(&self) -> u64 {
348        hardware_concurrency()
349    }
350
351    /// <https://w3c.github.io/clipboard-apis/#h-navigator-clipboard>
352    fn Clipboard(&self) -> DomRoot<Clipboard> {
353        self.clipboard
354            .or_init(|| Clipboard::new(&self.global(), CanGc::note()))
355    }
356
357    /// <https://w3c.github.io/beacon/#sec-processing-model>
358    fn SendBeacon(&self, url: USVString, data: Option<BodyInit>, can_gc: CanGc) -> Fallible<bool> {
359        let global = self.global();
360        // Step 1. Set base to this's relevant settings object's API base URL.
361        let base = global.api_base_url();
362        // Step 2. Set origin to this's relevant settings object's origin.
363        let origin = global.origin().immutable().clone();
364        // Step 3. Set parsedUrl to the result of the URL parser steps with url and base.
365        // If the algorithm returns an error, or if parsedUrl's scheme is not "http" or "https",
366        // throw a "TypeError" exception and terminate these steps.
367        let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
368            return Err(Error::Type("Cannot parse URL".to_owned()));
369        };
370        if !matches!(url.scheme(), "http" | "https") {
371            return Err(Error::Type("URL is not http(s)".to_owned()));
372        }
373        let mut request_body = None;
374        // Step 4. Let headerList be an empty list.
375        let mut headers = HeaderMap::with_capacity(1);
376        // Step 5. Let corsMode be "no-cors".
377        let mut cors_mode = RequestMode::NoCors;
378        // Step 6. If data is not null:
379        if let Some(data) = data {
380            // Step 6.1. Set transmittedData and contentType to the result of extracting data's byte stream
381            // with the keepalive flag set.
382            let extracted_body = data.extract(&global, can_gc)?;
383            // Step 6.2. If the amount of data that can be queued to be sent by keepalive enabled requests
384            // is exceeded by the size of transmittedData (as defined in HTTP-network-or-cache fetch),
385            // set the return value to false and terminate these steps.
386            if let Some(total_bytes) = extracted_body.total_bytes {
387                if total_bytes > 64 * 1024 {
388                    return Ok(false);
389                }
390            }
391            // Step 6.3. If contentType is not null:
392            if let Some(content_type) = extracted_body.content_type.as_ref() {
393                // Set corsMode to "cors".
394                cors_mode = RequestMode::CorsMode;
395                // If contentType value is a CORS-safelisted request-header value for the Content-Type header,
396                // set corsMode to "no-cors".
397                if is_cors_safelisted_request_content_type(content_type.as_bytes().deref()) {
398                    cors_mode = RequestMode::NoCors;
399                }
400                // Append a Content-Type header with value contentType to headerList.
401                //
402                // We cannot use typed header insertion with `mime::Mime` parsing here,
403                // since it lowercases `charset=UTF-8`: https://github.com/hyperium/mime/issues/116
404                headers.insert(
405                    header::CONTENT_TYPE,
406                    HeaderValue::from_str(&content_type.str()).unwrap(),
407                );
408            }
409            request_body = Some(extracted_body.into_net_request_body().0);
410        }
411        // Step 7.1. Let req be a new request, initialized as follows:
412        let request = RequestBuilder::new(None, url.clone(), global.get_referrer())
413            .mode(cors_mode)
414            .destination(Destination::None)
415            .policy_container(global.policy_container())
416            .insecure_requests_policy(global.insecure_requests_policy())
417            .has_trustworthy_ancestor_origin(global.has_trustworthy_ancestor_or_current_origin())
418            .method(http::Method::POST)
419            .body(request_body)
420            .origin(origin)
421            // TODO: Set keep-alive flag
422            .credentials_mode(CredentialsMode::Include)
423            .headers(headers);
424        // Step 7.2. Fetch req.
425        global.fetch(
426            request,
427            Arc::new(Mutex::new(BeaconFetchListener {
428                url,
429                global: Trusted::new(&global),
430                resource_timing: ResourceFetchTiming::new(ResourceTimingType::None),
431            })),
432            global.task_manager().networking_task_source().into(),
433        );
434        // Step 7. Set the return value to true, return the sendBeacon() call,
435        // and continue to run the following steps in parallel:
436        Ok(true)
437    }
438
439    /// <https://servo.org/internal-no-spec>
440    fn Servo(&self) -> DomRoot<ServoInternals> {
441        self.servo_internals
442            .or_init(|| ServoInternals::new(&self.global(), CanGc::note()))
443    }
444}
445
446struct BeaconFetchListener {
447    /// URL of this request.
448    url: ServoUrl,
449    /// Timing data for this resource.
450    resource_timing: ResourceFetchTiming,
451    /// The global object fetching the report uri violation
452    global: Trusted<GlobalScope>,
453}
454
455impl FetchResponseListener for BeaconFetchListener {
456    fn process_request_body(&mut self, _: RequestId) {}
457
458    fn process_request_eof(&mut self, _: RequestId) {}
459
460    fn process_response(
461        &mut self,
462        _: RequestId,
463        fetch_metadata: Result<FetchMetadata, NetworkError>,
464    ) {
465        _ = fetch_metadata;
466    }
467
468    fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
469        _ = chunk;
470    }
471
472    fn process_response_eof(
473        &mut self,
474        _: RequestId,
475        response: Result<ResourceFetchTiming, NetworkError>,
476    ) {
477        _ = response;
478    }
479
480    fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
481        &mut self.resource_timing
482    }
483
484    fn resource_timing(&self) -> &ResourceFetchTiming {
485        &self.resource_timing
486    }
487
488    fn submit_resource_timing(&mut self) {
489        submit_timing(self, CanGc::note())
490    }
491
492    fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
493        let global = self.resource_timing_global();
494        global.report_csp_violations(violations, None, None);
495    }
496}
497
498impl ResourceTimingListener for BeaconFetchListener {
499    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
500        (InitiatorType::Beacon, self.url.clone())
501    }
502
503    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
504        self.global.root()
505    }
506}
507
508impl PreInvoke for BeaconFetchListener {
509    fn should_invoke(&self) -> bool {
510        true
511    }
512}