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