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 base::generic_channel;
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::request::{
17    CredentialsMode, Destination, RequestBuilder, RequestId, RequestMode,
18    is_cors_safelisted_request_content_type,
19};
20use net_traits::{FetchMetadata, NetworkError, ResourceFetchTiming};
21use regex::Regex;
22use servo_config::pref;
23use servo_url::ServoUrl;
24
25use crate::body::Extractable;
26use crate::dom::bindings::cell::DomRefCell;
27use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods;
28use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
29use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit;
30use crate::dom::bindings::error::{Error, Fallible};
31use crate::dom::bindings::refcounted::Trusted;
32use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
33use crate::dom::bindings::root::{DomRoot, MutNullableDom};
34use crate::dom::bindings::str::{DOMString, USVString};
35use crate::dom::bindings::utils::to_frozen_array;
36#[cfg(feature = "bluetooth")]
37use crate::dom::bluetooth::Bluetooth;
38use crate::dom::clipboard::Clipboard;
39use crate::dom::credentialmanagement::credentialscontainer::CredentialsContainer;
40use crate::dom::csp::{GlobalCspReporting, Violation};
41use crate::dom::gamepad::Gamepad;
42use crate::dom::gamepad::gamepadevent::GamepadEventType;
43use crate::dom::geolocation::Geolocation;
44use crate::dom::globalscope::GlobalScope;
45use crate::dom::mediadevices::MediaDevices;
46use crate::dom::mediasession::MediaSession;
47use crate::dom::mimetypearray::MimeTypeArray;
48use crate::dom::navigatorinfo;
49use crate::dom::performance::performanceresourcetiming::InitiatorType;
50use crate::dom::permissions::Permissions;
51use crate::dom::pluginarray::PluginArray;
52use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
53use crate::dom::servointernals::ServoInternals;
54#[cfg(feature = "webgpu")]
55use crate::dom::webgpu::gpu::GPU;
56use crate::dom::window::Window;
57#[cfg(feature = "webxr")]
58use crate::dom::xrsystem::XRSystem;
59use crate::network_listener::{FetchResponseListener, ResourceTimingListener, submit_timing};
60use crate::script_runtime::{CanGc, JSContext};
61
62pub(super) fn hardware_concurrency() -> u64 {
63    static CPUS: LazyLock<u64> = LazyLock::new(|| num_cpus::get().try_into().unwrap_or(1));
64
65    *CPUS
66}
67
68/// <https://html.spec.whatwg.org/multipage/#safelisted-scheme>
69static SAFELISTED_SCHEMES: [&str; 24] = [
70    "bitcoin",
71    "ftp",
72    "ftps",
73    "geo",
74    "im",
75    "irc",
76    "ircs",
77    "magnet",
78    "mailto",
79    "matrix",
80    "mms",
81    "news",
82    "nntp",
83    "openpgp4fpr",
84    "sftp",
85    "sip",
86    "sms",
87    "smsto",
88    "ssh",
89    "tel",
90    "urn",
91    "webcal",
92    "wtai",
93    "xmpp",
94];
95
96/// Used in <https://html.spec.whatwg.org/multipage/#normalize-protocol-handler-parameters>
97fn matches_web_plus_protocol(scheme: &str) -> bool {
98    static WEB_PLUS_SCHEME_GRAMMAR: LazyLock<Regex> =
99        LazyLock::new(|| Regex::new(r#"^web\+[a-z]+$"#).unwrap());
100
101    WEB_PLUS_SCHEME_GRAMMAR.is_match(scheme)
102}
103
104#[dom_struct]
105pub(crate) struct Navigator {
106    reflector_: Reflector,
107    #[cfg(feature = "bluetooth")]
108    bluetooth: MutNullableDom<Bluetooth>,
109    credentials: MutNullableDom<CredentialsContainer>,
110    plugins: MutNullableDom<PluginArray>,
111    mime_types: MutNullableDom<MimeTypeArray>,
112    service_worker: MutNullableDom<ServiceWorkerContainer>,
113    #[cfg(feature = "webxr")]
114    xr: MutNullableDom<XRSystem>,
115    mediadevices: MutNullableDom<MediaDevices>,
116    /// <https://www.w3.org/TR/gamepad/#dfn-gamepads>
117    gamepads: DomRefCell<Vec<MutNullableDom<Gamepad>>>,
118    permissions: MutNullableDom<Permissions>,
119    mediasession: MutNullableDom<MediaSession>,
120    clipboard: MutNullableDom<Clipboard>,
121    #[cfg(feature = "webgpu")]
122    gpu: MutNullableDom<GPU>,
123    /// <https://www.w3.org/TR/gamepad/#dfn-hasgamepadgesture>
124    has_gamepad_gesture: Cell<bool>,
125    servo_internals: MutNullableDom<ServoInternals>,
126}
127
128impl Navigator {
129    fn new_inherited() -> Navigator {
130        Navigator {
131            reflector_: Reflector::new(),
132            #[cfg(feature = "bluetooth")]
133            bluetooth: Default::default(),
134            credentials: Default::default(),
135            plugins: Default::default(),
136            mime_types: Default::default(),
137            service_worker: Default::default(),
138            #[cfg(feature = "webxr")]
139            xr: Default::default(),
140            mediadevices: Default::default(),
141            gamepads: Default::default(),
142            permissions: Default::default(),
143            mediasession: Default::default(),
144            clipboard: Default::default(),
145            #[cfg(feature = "webgpu")]
146            gpu: Default::default(),
147            has_gamepad_gesture: Cell::new(false),
148            servo_internals: Default::default(),
149        }
150    }
151
152    pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<Navigator> {
153        reflect_dom_object(Box::new(Navigator::new_inherited()), window, can_gc)
154    }
155
156    #[cfg(feature = "webxr")]
157    pub(crate) fn xr(&self) -> Option<DomRoot<XRSystem>> {
158        self.xr.get()
159    }
160
161    pub(crate) fn get_gamepad(&self, index: usize) -> Option<DomRoot<Gamepad>> {
162        self.gamepads.borrow().get(index).and_then(|g| g.get())
163    }
164
165    pub(crate) fn set_gamepad(&self, index: usize, gamepad: &Gamepad, can_gc: CanGc) {
166        if let Some(gamepad_to_set) = self.gamepads.borrow().get(index) {
167            gamepad_to_set.set(Some(gamepad));
168        }
169        if self.has_gamepad_gesture.get() {
170            gamepad.set_exposed(true);
171            if self.global().as_window().Document().is_fully_active() {
172                gamepad.notify_event(GamepadEventType::Connected, can_gc);
173            }
174        }
175    }
176
177    pub(crate) fn remove_gamepad(&self, index: usize) {
178        if let Some(gamepad_to_remove) = self.gamepads.borrow_mut().get(index) {
179            gamepad_to_remove.set(None);
180        }
181        self.shrink_gamepads_list();
182    }
183
184    /// <https://www.w3.org/TR/gamepad/#dfn-selecting-an-unused-gamepad-index>
185    pub(crate) fn select_gamepad_index(&self) -> u32 {
186        let mut gamepad_list = self.gamepads.borrow_mut();
187        if let Some(index) = gamepad_list.iter().position(|g| g.get().is_none()) {
188            index as u32
189        } else {
190            let len = gamepad_list.len();
191            gamepad_list.resize_with(len + 1, Default::default);
192            len as u32
193        }
194    }
195
196    fn shrink_gamepads_list(&self) {
197        let mut gamepad_list = self.gamepads.borrow_mut();
198        for i in (0..gamepad_list.len()).rev() {
199            if gamepad_list.get(i).is_none() {
200                gamepad_list.remove(i);
201            } else {
202                break;
203            }
204        }
205    }
206
207    pub(crate) fn has_gamepad_gesture(&self) -> bool {
208        self.has_gamepad_gesture.get()
209    }
210
211    pub(crate) fn set_has_gamepad_gesture(&self, has_gamepad_gesture: bool) {
212        self.has_gamepad_gesture.set(has_gamepad_gesture);
213    }
214
215    /// <https://html.spec.whatwg.org/multipage/#normalize-protocol-handler-parameters>
216    fn normalize_protocol_handler_parameters(
217        &self,
218        scheme: DOMString,
219        url: USVString,
220    ) -> Fallible<(String, ServoUrl)> {
221        // Step 1. Set scheme to scheme, converted to ASCII lowercase.
222        let scheme = scheme.to_ascii_lowercase();
223        // Step 2. If scheme is neither a safelisted scheme nor
224        // a string starting with "web+" followed by one or more ASCII lower alphas, then throw a "SecurityError" DOMException.
225        if !SAFELISTED_SCHEMES.contains(&scheme.as_ref()) && !matches_web_plus_protocol(&scheme) {
226            return Err(Error::Security(None));
227        }
228        // Step 3. If url does not contain "%s", then throw a "SyntaxError" DOMException.
229        if !url.contains("%s") {
230            return Err(Error::Syntax(Some(
231                "Missing replacement string %s in URL".to_owned(),
232            )));
233        }
234        // Step 4. Let urlRecord be the result of encoding-parsing a URL given url, relative to environment.
235        let environment = self.global();
236        // Navigator is only exposed on Window, so this is safe to do
237        let window = environment.as_window();
238        let Ok(url) = window.Document().encoding_parse_a_url(&url) else {
239            // Step 5. If urlRecord is failure, then throw a "SyntaxError" DOMException.
240            return Err(Error::Syntax(Some("Cannot parse URL".to_owned())));
241        };
242        // Step 6. If urlRecord's scheme is not an HTTP(S) scheme or urlRecord's origin
243        // is not same origin with environment's origin, then throw a "SecurityError" DOMException.
244        if !matches!(url.scheme(), "http" | "https") {
245            return Err(Error::Security(None));
246        }
247        let environment_origin = environment.origin().immutable().clone();
248        if url.origin() != environment_origin {
249            return Err(Error::Security(None));
250        }
251        // Step 7. Assert: the result of Is url potentially trustworthy? given urlRecord is "Potentially Trustworthy".
252        assert!(url.is_potentially_trustworthy());
253        // Step 8. Return (scheme, urlRecord).
254        Ok((scheme, url))
255    }
256
257    fn send_protocol_update_registration_to_embedder(
258        &self,
259        registration: ProtocolHandlerUpdateRegistration,
260    ) {
261        let global = self.global();
262        let window = global.as_window();
263        let (sender, _) = generic_channel::channel().unwrap();
264        let _ = global
265            .script_to_embedder_chan()
266            .send(EmbedderMsg::AllowProtocolHandlerRequest(
267                window.webview_id(),
268                registration,
269                sender,
270            ));
271    }
272}
273
274impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
275    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-product>
276    fn Product(&self) -> DOMString {
277        navigatorinfo::Product()
278    }
279
280    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-productsub>
281    fn ProductSub(&self) -> DOMString {
282        navigatorinfo::ProductSub()
283    }
284
285    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-vendor>
286    fn Vendor(&self) -> DOMString {
287        navigatorinfo::Vendor()
288    }
289
290    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-vendorsub>
291    fn VendorSub(&self) -> DOMString {
292        navigatorinfo::VendorSub()
293    }
294
295    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-taintenabled>
296    fn TaintEnabled(&self) -> bool {
297        navigatorinfo::TaintEnabled()
298    }
299
300    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-appname>
301    fn AppName(&self) -> DOMString {
302        navigatorinfo::AppName()
303    }
304
305    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-appcodename>
306    fn AppCodeName(&self) -> DOMString {
307        navigatorinfo::AppCodeName()
308    }
309
310    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-platform>
311    fn Platform(&self) -> DOMString {
312        navigatorinfo::Platform()
313    }
314
315    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-useragent>
316    fn UserAgent(&self) -> DOMString {
317        navigatorinfo::UserAgent(&pref!(user_agent))
318    }
319
320    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-appversion>
321    fn AppVersion(&self) -> DOMString {
322        navigatorinfo::AppVersion()
323    }
324
325    // https://webbluetoothcg.github.io/web-bluetooth/#dom-navigator-bluetooth
326    #[cfg(feature = "bluetooth")]
327    fn Bluetooth(&self) -> DomRoot<Bluetooth> {
328        self.bluetooth
329            .or_init(|| Bluetooth::new(&self.global(), CanGc::note()))
330    }
331
332    /// <https://www.w3.org/TR/credential-management-1/#framework-credential-management>
333    fn Credentials(&self) -> DomRoot<CredentialsContainer> {
334        self.credentials
335            .or_init(|| CredentialsContainer::new(&self.global(), CanGc::note()))
336    }
337
338    /// <https://www.w3.org/TR/geolocation/#navigator_interface>
339    fn Geolocation(&self) -> DomRoot<Geolocation> {
340        Geolocation::new(&self.global(), CanGc::note())
341    }
342
343    /// <https://html.spec.whatwg.org/multipage/#navigatorlanguage>
344    fn Language(&self) -> DOMString {
345        navigatorinfo::Language()
346    }
347
348    // https://html.spec.whatwg.org/multipage/#dom-navigator-languages
349    fn Languages(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
350        to_frozen_array(&[self.Language()], cx, retval, can_gc)
351    }
352
353    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-online>
354    fn OnLine(&self) -> bool {
355        true
356    }
357
358    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-plugins>
359    fn Plugins(&self) -> DomRoot<PluginArray> {
360        self.plugins
361            .or_init(|| PluginArray::new(&self.global(), CanGc::note()))
362    }
363
364    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-mimetypes>
365    fn MimeTypes(&self) -> DomRoot<MimeTypeArray> {
366        self.mime_types
367            .or_init(|| MimeTypeArray::new(&self.global(), CanGc::note()))
368    }
369
370    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-javaenabled>
371    fn JavaEnabled(&self) -> bool {
372        false
373    }
374
375    /// <https://w3c.github.io/ServiceWorker/#navigator-service-worker-attribute>
376    fn ServiceWorker(&self) -> DomRoot<ServiceWorkerContainer> {
377        self.service_worker
378            .or_init(|| ServiceWorkerContainer::new(&self.global(), CanGc::note()))
379    }
380
381    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-cookieenabled>
382    fn CookieEnabled(&self) -> bool {
383        true
384    }
385
386    /// <https://www.w3.org/TR/gamepad/#dom-navigator-getgamepads>
387    fn GetGamepads(&self) -> Vec<Option<DomRoot<Gamepad>>> {
388        let global = self.global();
389        let window = global.as_window();
390        let doc = window.Document();
391
392        // TODO: Handle permissions policy once implemented
393        if !doc.is_fully_active() || !self.has_gamepad_gesture.get() {
394            return Vec::new();
395        }
396
397        self.gamepads.borrow().iter().map(|g| g.get()).collect()
398    }
399    /// <https://w3c.github.io/permissions/#navigator-and-workernavigator-extension>
400    fn Permissions(&self) -> DomRoot<Permissions> {
401        self.permissions
402            .or_init(|| Permissions::new(&self.global(), CanGc::note()))
403    }
404
405    /// <https://immersive-web.github.io/webxr/#dom-navigator-xr>
406    #[cfg(feature = "webxr")]
407    fn Xr(&self) -> DomRoot<XRSystem> {
408        self.xr
409            .or_init(|| XRSystem::new(self.global().as_window(), CanGc::note()))
410    }
411
412    /// <https://w3c.github.io/mediacapture-main/#dom-navigator-mediadevices>
413    fn MediaDevices(&self) -> DomRoot<MediaDevices> {
414        self.mediadevices
415            .or_init(|| MediaDevices::new(&self.global(), CanGc::note()))
416    }
417
418    /// <https://w3c.github.io/mediasession/#dom-navigator-mediasession>
419    fn MediaSession(&self) -> DomRoot<MediaSession> {
420        self.mediasession.or_init(|| {
421            // There is a single MediaSession instance per Pipeline
422            // and only one active MediaSession globally.
423            //
424            // MediaSession creation can happen in two cases:
425            //
426            // - If content gets `navigator.mediaSession`
427            // - If a media instance (HTMLMediaElement so far) starts playing media.
428            let global = self.global();
429            let window = global.as_window();
430            MediaSession::new(window, CanGc::note())
431        })
432    }
433
434    // https://gpuweb.github.io/gpuweb/#dom-navigator-gpu
435    #[cfg(feature = "webgpu")]
436    fn Gpu(&self) -> DomRoot<GPU> {
437        self.gpu.or_init(|| GPU::new(&self.global(), CanGc::note()))
438    }
439
440    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-hardwareconcurrency>
441    fn HardwareConcurrency(&self) -> u64 {
442        hardware_concurrency()
443    }
444
445    /// <https://w3c.github.io/clipboard-apis/#h-navigator-clipboard>
446    fn Clipboard(&self) -> DomRoot<Clipboard> {
447        self.clipboard
448            .or_init(|| Clipboard::new(&self.global(), CanGc::note()))
449    }
450
451    /// <https://w3c.github.io/beacon/#sec-processing-model>
452    fn SendBeacon(&self, url: USVString, data: Option<BodyInit>, can_gc: CanGc) -> Fallible<bool> {
453        let global = self.global();
454        // Step 1. Set base to this's relevant settings object's API base URL.
455        let base = global.api_base_url();
456        // Step 2. Set origin to this's relevant settings object's origin.
457        let origin = global.origin().immutable().clone();
458        // Step 3. Set parsedUrl to the result of the URL parser steps with url and base.
459        // If the algorithm returns an error, or if parsedUrl's scheme is not "http" or "https",
460        // throw a "TypeError" exception and terminate these steps.
461        let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
462            return Err(Error::Type("Cannot parse URL".to_owned()));
463        };
464        if !matches!(url.scheme(), "http" | "https") {
465            return Err(Error::Type("URL is not http(s)".to_owned()));
466        }
467        let mut request_body = None;
468        // Step 4. Let headerList be an empty list.
469        let mut headers = HeaderMap::with_capacity(1);
470        // Step 5. Let corsMode be "no-cors".
471        let mut cors_mode = RequestMode::NoCors;
472        // Step 6. If data is not null:
473        if let Some(data) = data {
474            // Step 6.1. Set transmittedData and contentType to the result of extracting data's byte stream
475            // with the keepalive flag set.
476            let extracted_body = data.extract(&global, can_gc)?;
477            // Step 6.2. If the amount of data that can be queued to be sent by keepalive enabled requests
478            // is exceeded by the size of transmittedData (as defined in HTTP-network-or-cache fetch),
479            // set the return value to false and terminate these steps.
480            if let Some(total_bytes) = extracted_body.total_bytes {
481                if total_bytes > 64 * 1024 {
482                    return Ok(false);
483                }
484            }
485            // Step 6.3. If contentType is not null:
486            if let Some(content_type) = extracted_body.content_type.as_ref() {
487                // Set corsMode to "cors".
488                cors_mode = RequestMode::CorsMode;
489                // If contentType value is a CORS-safelisted request-header value for the Content-Type header,
490                // set corsMode to "no-cors".
491                if is_cors_safelisted_request_content_type(content_type.as_bytes().deref()) {
492                    cors_mode = RequestMode::NoCors;
493                }
494                // Append a Content-Type header with value contentType to headerList.
495                //
496                // We cannot use typed header insertion with `mime::Mime` parsing here,
497                // since it lowercases `charset=UTF-8`: https://github.com/hyperium/mime/issues/116
498                headers.insert(
499                    header::CONTENT_TYPE,
500                    HeaderValue::from_str(&content_type.str()).unwrap(),
501                );
502            }
503            request_body = Some(extracted_body.into_net_request_body().0);
504        }
505        // Step 7.1. Let req be a new request, initialized as follows:
506        let request = RequestBuilder::new(None, url.clone(), global.get_referrer())
507            .mode(cors_mode)
508            .destination(Destination::None)
509            .policy_container(global.policy_container())
510            .insecure_requests_policy(global.insecure_requests_policy())
511            .has_trustworthy_ancestor_origin(global.has_trustworthy_ancestor_or_current_origin())
512            .method(http::Method::POST)
513            .body(request_body)
514            .origin(origin)
515            // TODO: Set keep-alive flag
516            .credentials_mode(CredentialsMode::Include)
517            .headers(headers);
518        // Step 7.2. Fetch req.
519        global.fetch(
520            request,
521            BeaconFetchListener {
522                url,
523                global: Trusted::new(&global),
524            },
525            global.task_manager().networking_task_source().into(),
526        );
527        // Step 7. Set the return value to true, return the sendBeacon() call,
528        // and continue to run the following steps in parallel:
529        Ok(true)
530    }
531
532    /// <https://servo.org/internal-no-spec>
533    fn Servo(&self) -> DomRoot<ServoInternals> {
534        self.servo_internals
535            .or_init(|| ServoInternals::new(&self.global(), CanGc::note()))
536    }
537
538    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-registerprotocolhandler>
539    fn RegisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
540        // Step 1. Let (normalizedScheme, normalizedURLString) be the result of
541        // running normalize protocol handler parameters with scheme, url, and this's relevant settings object.
542        let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
543        // Step 2. In parallel: register a protocol handler for normalizedScheme and normalizedURLString.
544        // User agents may, within the constraints described, do whatever they like. A user agent could,
545        // for instance, prompt the user and offer the user the opportunity to add the site to a shortlist of handlers,
546        // or make the handlers their default, or cancel the request. User agents could also silently collect the information,
547        // providing it only when relevant to the user.
548        // User agents should keep track of which sites have registered handlers (even if the user has declined such registrations)
549        // so that the user is not repeatedly prompted with the same request.
550        // If the registerProtocolHandler() automation mode of this's relevant global object's associated Document is not "none",
551        // the user agent should first verify that it is in an automation context (see WebDriver's security considerations).
552        // The user agent should then bypass the above communication of information and gathering of user consent,
553        // and instead do the following based on the value of the registerProtocolHandler() automation mode:
554        self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
555            scheme,
556            url,
557            register_or_unregister: RegisterOrUnregister::Register,
558        });
559        Ok(())
560    }
561
562    /// <https://html.spec.whatwg.org/multipage/#dom-navigator-unregisterprotocolhandler>
563    fn UnregisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
564        // Step 1. Let (normalizedScheme, normalizedURLString) be the result of
565        // running normalize protocol handler parameters with scheme, url, and this's relevant settings object.
566        let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
567        // Step 2. In parallel: unregister the handler described by normalizedScheme and normalizedURLString.
568        self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
569            scheme,
570            url,
571            register_or_unregister: RegisterOrUnregister::Unregister,
572        });
573        Ok(())
574    }
575}
576
577struct BeaconFetchListener {
578    /// URL of this request.
579    url: ServoUrl,
580    /// The global object fetching the report uri violation
581    global: Trusted<GlobalScope>,
582}
583
584impl FetchResponseListener for BeaconFetchListener {
585    fn process_request_body(&mut self, _: RequestId) {}
586
587    fn process_request_eof(&mut self, _: RequestId) {}
588
589    fn process_response(
590        &mut self,
591        _: RequestId,
592        fetch_metadata: Result<FetchMetadata, NetworkError>,
593    ) {
594        _ = fetch_metadata;
595    }
596
597    fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
598        _ = chunk;
599    }
600
601    fn process_response_eof(
602        self,
603        _: RequestId,
604        response: Result<ResourceFetchTiming, NetworkError>,
605    ) {
606        if let Ok(response) = response {
607            submit_timing(&self, &response, CanGc::note());
608        }
609    }
610
611    fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
612        let global = self.resource_timing_global();
613        global.report_csp_violations(violations, None, None);
614    }
615}
616
617impl ResourceTimingListener for BeaconFetchListener {
618    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
619        (InitiatorType::Beacon, self.url.clone())
620    }
621
622    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
623        self.global.root()
624    }
625}