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