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::LazyLock;
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::{FetchMetadata, NetworkError, ResourceFetchTiming};
19use servo_config::pref;
20use servo_url::ServoUrl;
21
22use crate::body::Extractable;
23use crate::dom::bindings::cell::DomRefCell;
24use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods;
25use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
26use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit;
27use crate::dom::bindings::error::{Error, Fallible};
28use crate::dom::bindings::refcounted::Trusted;
29use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
30use crate::dom::bindings::root::{DomRoot, MutNullableDom};
31use crate::dom::bindings::str::{DOMString, USVString};
32use crate::dom::bindings::utils::to_frozen_array;
33#[cfg(feature = "bluetooth")]
34use crate::dom::bluetooth::Bluetooth;
35use crate::dom::clipboard::Clipboard;
36use crate::dom::credentialmanagement::credentialscontainer::CredentialsContainer;
37use crate::dom::csp::{GlobalCspReporting, Violation};
38use crate::dom::gamepad::Gamepad;
39use crate::dom::gamepad::gamepadevent::GamepadEventType;
40use crate::dom::geolocation::Geolocation;
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::performance::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::{FetchResponseListener, 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://www.w3.org/TR/geolocation/#navigator_interface>
242    fn Geolocation(&self) -> DomRoot<Geolocation> {
243        Geolocation::new(&self.global(), CanGc::note())
244    }
245
246    /// <https://html.spec.whatwg.org/multipage/#navigatorlanguage>
247    fn Language(&self) -> DOMString {
248        navigatorinfo::Language()
249    }
250
251    // https://html.spec.whatwg.org/multipage/#dom-navigator-languages
252    fn Languages(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
253        to_frozen_array(&[self.Language()], cx, retval, can_gc)
254    }
255
256    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-online>
257    fn OnLine(&self) -> bool {
258        true
259    }
260
261    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-plugins>
262    fn Plugins(&self) -> DomRoot<PluginArray> {
263        self.plugins
264            .or_init(|| PluginArray::new(&self.global(), CanGc::note()))
265    }
266
267    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-mimetypes>
268    fn MimeTypes(&self) -> DomRoot<MimeTypeArray> {
269        self.mime_types
270            .or_init(|| MimeTypeArray::new(&self.global(), CanGc::note()))
271    }
272
273    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-javaenabled>
274    fn JavaEnabled(&self) -> bool {
275        false
276    }
277
278    /// <https://w3c.github.io/ServiceWorker/#navigator-service-worker-attribute>
279    fn ServiceWorker(&self) -> DomRoot<ServiceWorkerContainer> {
280        self.service_worker
281            .or_init(|| ServiceWorkerContainer::new(&self.global(), CanGc::note()))
282    }
283
284    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-cookieenabled>
285    fn CookieEnabled(&self) -> bool {
286        true
287    }
288
289    /// <https://www.w3.org/TR/gamepad/#dom-navigator-getgamepads>
290    fn GetGamepads(&self) -> Vec<Option<DomRoot<Gamepad>>> {
291        let global = self.global();
292        let window = global.as_window();
293        let doc = window.Document();
294
295        // TODO: Handle permissions policy once implemented
296        if !doc.is_fully_active() || !self.has_gamepad_gesture.get() {
297            return Vec::new();
298        }
299
300        self.gamepads.borrow().iter().map(|g| g.get()).collect()
301    }
302    /// <https://w3c.github.io/permissions/#navigator-and-workernavigator-extension>
303    fn Permissions(&self) -> DomRoot<Permissions> {
304        self.permissions
305            .or_init(|| Permissions::new(&self.global(), CanGc::note()))
306    }
307
308    /// <https://immersive-web.github.io/webxr/#dom-navigator-xr>
309    #[cfg(feature = "webxr")]
310    fn Xr(&self) -> DomRoot<XRSystem> {
311        self.xr
312            .or_init(|| XRSystem::new(self.global().as_window(), CanGc::note()))
313    }
314
315    /// <https://w3c.github.io/mediacapture-main/#dom-navigator-mediadevices>
316    fn MediaDevices(&self) -> DomRoot<MediaDevices> {
317        self.mediadevices
318            .or_init(|| MediaDevices::new(&self.global(), CanGc::note()))
319    }
320
321    /// <https://w3c.github.io/mediasession/#dom-navigator-mediasession>
322    fn MediaSession(&self) -> DomRoot<MediaSession> {
323        self.mediasession.or_init(|| {
324            // There is a single MediaSession instance per Pipeline
325            // and only one active MediaSession globally.
326            //
327            // MediaSession creation can happen in two cases:
328            //
329            // - If content gets `navigator.mediaSession`
330            // - If a media instance (HTMLMediaElement so far) starts playing media.
331            let global = self.global();
332            let window = global.as_window();
333            MediaSession::new(window, CanGc::note())
334        })
335    }
336
337    // https://gpuweb.github.io/gpuweb/#dom-navigator-gpu
338    #[cfg(feature = "webgpu")]
339    fn Gpu(&self) -> DomRoot<GPU> {
340        self.gpu.or_init(|| GPU::new(&self.global(), CanGc::note()))
341    }
342
343    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-hardwareconcurrency>
344    fn HardwareConcurrency(&self) -> u64 {
345        hardware_concurrency()
346    }
347
348    /// <https://w3c.github.io/clipboard-apis/#h-navigator-clipboard>
349    fn Clipboard(&self) -> DomRoot<Clipboard> {
350        self.clipboard
351            .or_init(|| Clipboard::new(&self.global(), CanGc::note()))
352    }
353
354    /// <https://w3c.github.io/beacon/#sec-processing-model>
355    fn SendBeacon(&self, url: USVString, data: Option<BodyInit>, can_gc: CanGc) -> Fallible<bool> {
356        let global = self.global();
357        // Step 1. Set base to this's relevant settings object's API base URL.
358        let base = global.api_base_url();
359        // Step 2. Set origin to this's relevant settings object's origin.
360        let origin = global.origin().immutable().clone();
361        // Step 3. Set parsedUrl to the result of the URL parser steps with url and base.
362        // If the algorithm returns an error, or if parsedUrl's scheme is not "http" or "https",
363        // throw a "TypeError" exception and terminate these steps.
364        let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
365            return Err(Error::Type("Cannot parse URL".to_owned()));
366        };
367        if !matches!(url.scheme(), "http" | "https") {
368            return Err(Error::Type("URL is not http(s)".to_owned()));
369        }
370        let mut request_body = None;
371        // Step 4. Let headerList be an empty list.
372        let mut headers = HeaderMap::with_capacity(1);
373        // Step 5. Let corsMode be "no-cors".
374        let mut cors_mode = RequestMode::NoCors;
375        // Step 6. If data is not null:
376        if let Some(data) = data {
377            // Step 6.1. Set transmittedData and contentType to the result of extracting data's byte stream
378            // with the keepalive flag set.
379            let extracted_body = data.extract(&global, can_gc)?;
380            // Step 6.2. If the amount of data that can be queued to be sent by keepalive enabled requests
381            // is exceeded by the size of transmittedData (as defined in HTTP-network-or-cache fetch),
382            // set the return value to false and terminate these steps.
383            if let Some(total_bytes) = extracted_body.total_bytes {
384                if total_bytes > 64 * 1024 {
385                    return Ok(false);
386                }
387            }
388            // Step 6.3. If contentType is not null:
389            if let Some(content_type) = extracted_body.content_type.as_ref() {
390                // Set corsMode to "cors".
391                cors_mode = RequestMode::CorsMode;
392                // If contentType value is a CORS-safelisted request-header value for the Content-Type header,
393                // set corsMode to "no-cors".
394                if is_cors_safelisted_request_content_type(content_type.as_bytes().deref()) {
395                    cors_mode = RequestMode::NoCors;
396                }
397                // Append a Content-Type header with value contentType to headerList.
398                //
399                // We cannot use typed header insertion with `mime::Mime` parsing here,
400                // since it lowercases `charset=UTF-8`: https://github.com/hyperium/mime/issues/116
401                headers.insert(
402                    header::CONTENT_TYPE,
403                    HeaderValue::from_str(&content_type.str()).unwrap(),
404                );
405            }
406            request_body = Some(extracted_body.into_net_request_body().0);
407        }
408        // Step 7.1. Let req be a new request, initialized as follows:
409        let request = RequestBuilder::new(None, url.clone(), global.get_referrer())
410            .mode(cors_mode)
411            .destination(Destination::None)
412            .policy_container(global.policy_container())
413            .insecure_requests_policy(global.insecure_requests_policy())
414            .has_trustworthy_ancestor_origin(global.has_trustworthy_ancestor_or_current_origin())
415            .method(http::Method::POST)
416            .body(request_body)
417            .origin(origin)
418            // TODO: Set keep-alive flag
419            .credentials_mode(CredentialsMode::Include)
420            .headers(headers);
421        // Step 7.2. Fetch req.
422        global.fetch(
423            request,
424            BeaconFetchListener {
425                url,
426                global: Trusted::new(&global),
427            },
428            global.task_manager().networking_task_source().into(),
429        );
430        // Step 7. Set the return value to true, return the sendBeacon() call,
431        // and continue to run the following steps in parallel:
432        Ok(true)
433    }
434
435    /// <https://servo.org/internal-no-spec>
436    fn Servo(&self) -> DomRoot<ServoInternals> {
437        self.servo_internals
438            .or_init(|| ServoInternals::new(&self.global(), CanGc::note()))
439    }
440}
441
442struct BeaconFetchListener {
443    /// URL of this request.
444    url: ServoUrl,
445    /// The global object fetching the report uri violation
446    global: Trusted<GlobalScope>,
447}
448
449impl FetchResponseListener for BeaconFetchListener {
450    fn process_request_body(&mut self, _: RequestId) {}
451
452    fn process_request_eof(&mut self, _: RequestId) {}
453
454    fn process_response(
455        &mut self,
456        _: RequestId,
457        fetch_metadata: Result<FetchMetadata, NetworkError>,
458    ) {
459        _ = fetch_metadata;
460    }
461
462    fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
463        _ = chunk;
464    }
465
466    fn process_response_eof(
467        self,
468        _: RequestId,
469        response: Result<ResourceFetchTiming, NetworkError>,
470    ) {
471        if let Ok(response) = response {
472            submit_timing(&self, &response, CanGc::note());
473        }
474    }
475
476    fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
477        let global = self.resource_timing_global();
478        global.report_csp_violations(violations, None, None);
479    }
480}
481
482impl ResourceTimingListener for BeaconFetchListener {
483    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
484        (InitiatorType::Beacon, self.url.clone())
485    }
486
487    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
488        self.global.root()
489    }
490}