diff --git a/components/script/dom/htmllinkelement.rs b/components/script/dom/htmllinkelement.rs index 77c33b2ab2c..34f95116b74 100644 --- a/components/script/dom/htmllinkelement.rs +++ b/components/script/dom/htmllinkelement.rs @@ -11,8 +11,13 @@ use base::id::WebViewId; use dom_struct::dom_struct; use embedder_traits::EmbedderMsg; use html5ever::{LocalName, Prefix, local_name, ns}; +use ipc_channel::ipc::IpcSender; use js::rust::HandleObject; use mime::Mime; +use net_traits::image_cache::{ + Image, ImageCache, ImageCacheResponseMessage, ImageCacheResult, ImageLoadListener, + ImageOrMetadataAvailable, ImageResponse, PendingImageId, UsePlaceholder, +}; use net_traits::mime_classifier::{MediaType, MimeClassifier}; use net_traits::policy_container::PolicyContainer; use net_traits::request::{ @@ -20,15 +25,17 @@ use net_traits::request::{ RequestId, }; use net_traits::{ - FetchMetadata, FetchResponseListener, NetworkError, ReferrerPolicy, ResourceFetchTiming, - ResourceTimingType, + FetchMetadata, FetchResponseListener, FetchResponseMsg, NetworkError, ReferrerPolicy, + ResourceFetchTiming, ResourceTimingType, }; +use pixels::PixelFormat; use script_bindings::root::Dom; use servo_arc::Arc; use servo_url::{ImmutableOrigin, ServoUrl}; use style::attr::AttrValue; use style::stylesheets::Stylesheet; use stylo_atoms::Atom; +use webrender_api::units::DeviceIntSize; use crate::dom::attr::Attr; use crate::dom::bindings::cell::DomRefCell; @@ -272,8 +279,7 @@ impl VirtualMethods for HTMLLinkElement { } if self.relations.get().contains(LinkRelations::ICON) { - let sizes = get_attr(self.upcast(), &local_name!("sizes")); - self.handle_favicon_url(&attr.value(), &sizes); + self.handle_favicon_url(); } // https://html.spec.whatwg.org/multipage/#link-type-prefetch @@ -291,9 +297,7 @@ impl VirtualMethods for HTMLLinkElement { } }, local_name!("sizes") if self.relations.get().contains(LinkRelations::ICON) => { - if let Some(ref href) = get_attr(self.upcast(), &local_name!("href")) { - self.handle_favicon_url(href, &Some(attr.value().to_string())); - } + self.handle_favicon_url(); }, local_name!("crossorigin") => { // https://html.spec.whatwg.org/multipage/#link-type-prefetch @@ -396,8 +400,7 @@ impl VirtualMethods for HTMLLinkElement { } if relations.contains(LinkRelations::ICON) { - let sizes = get_attr(self.upcast(), &local_name!("sizes")); - self.handle_favicon_url(&href, &sizes); + self.handle_favicon_url(); } if relations.contains(LinkRelations::PREFETCH) { @@ -483,6 +486,48 @@ impl HTMLLinkElement { options } + /// + /// + /// This method does not implement Step 7 (fetching the request) and instead returns the [RequestBuilder], + /// as the fetch context that should be used depends on the link type. + fn default_fetch_and_process_the_linked_resource(&self) -> Option { + // Step 1. Let options be the result of creating link options from el. + let options = self.processing_options(); + + // Step 2. Let request be the result of creating a link request given options. + let Some(request) = options.create_link_request(self.owner_window().webview_id()) else { + // Step 3. If request is null, then return. + return None; + }; + // Step 4. Set request's synchronous flag. + let mut request = request.synchronous(true); + + // Step 5. Run the linked resource fetch setup steps, given el and request. If the result is false, then return. + if !self.linked_resource_fetch_setup(&mut request) { + return None; + } + + // TODO Step 6. Set request's initiator type to "css" if el's rel attribute + // contains the keyword stylesheet; "link" otherwise. + + // Step 7. Fetch request with processResponseConsumeBody set to the following steps given response response and null, + // failure, or a byte sequence bodyBytes: [..] + Some(request) + } + + /// + fn linked_resource_fetch_setup(&self, request: &mut RequestBuilder) -> bool { + if self.relations.get().contains(LinkRelations::ICON) { + // Step 1. Set request's destination to "image". + request.destination = Destination::Image; + + // Step 2. Return true. + return true; + } + + true + } + /// The `fetch and process the linked resource` algorithm for [`rel="prefetch"`](https://html.spec.whatwg.org/multipage/#link-type-prefetch) fn fetch_and_process_prefetch_link(&self, href: &str) { // Step 1. If el's href attribute's value is the empty string, then return. @@ -589,17 +634,162 @@ impl HTMLLinkElement { } } - fn handle_favicon_url(&self, href: &str, _sizes: &Option) { - let document = self.owner_document(); - match document.base_url().join(href) { - Ok(url) => { - let window = document.window(); - if window.is_top_level() { - let msg = EmbedderMsg::NewFavicon(document.webview_id(), url.clone()); - window.send_to_embedder(msg); + fn handle_favicon_url(&self) { + // The spec does not specify this, but we don't fetch favicons for iframes, as + // they won't be displayed anyways. + let window = self.owner_window(); + if !window.is_top_level() { + return; + } + let Ok(href) = self.Href().parse() else { + return; + }; + + // Ignore all previous fetch operations + self.request_generation_id + .set(self.request_generation_id.get().increment()); + + let cache_result = window.image_cache().get_cached_image_status( + href, + window.origin().immutable().clone(), + cors_setting_for_element(self.upcast()), + UsePlaceholder::No, + ); + + match cache_result { + ImageCacheResult::Available(ImageOrMetadataAvailable::ImageAvailable { + image, + is_placeholder, + .. + }) => { + debug_assert!(!is_placeholder); + + self.process_favicon_response(image); + }, + ImageCacheResult::Available(ImageOrMetadataAvailable::MetadataAvailable(_, id)) | + ImageCacheResult::Pending(id) => { + let sender = self.register_image_cache_callback(id); + window.image_cache().add_listener(ImageLoadListener::new( + sender, + window.pipeline_id(), + id, + )); + }, + ImageCacheResult::ReadyForRequest(id) => { + let Some(request) = self.default_fetch_and_process_the_linked_resource() else { + return; + }; + + let sender = self.register_image_cache_callback(id); + window.image_cache().add_listener(ImageLoadListener::new( + sender, + window.pipeline_id(), + id, + )); + + let document = self.upcast::().owner_doc(); + let fetch_context = FaviconFetchContext { + url: self.owner_document().base_url(), + image_cache: window.image_cache(), + id, + link: Trusted::new(self), + resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource), + }; + document.fetch_background(request, fetch_context); + }, + ImageCacheResult::LoadError => {}, + }; + } + + fn register_image_cache_callback( + &self, + id: PendingImageId, + ) -> IpcSender { + let trusted_node = Trusted::new(self); + let window = self.owner_window(); + let request_generation_id = self.get_request_generation_id(); + window.register_image_cache_listener(id, move |response| { + let trusted_node = trusted_node.clone(); + let link_element = trusted_node.root(); + let window = link_element.owner_window(); + + let ImageResponse::Loaded(image, _) = response.response else { + // We don't care about metadata and such for favicons. + return; + }; + + if request_generation_id != link_element.get_request_generation_id() { + // This load is no longer relevant. + return; + }; + + window + .as_global_scope() + .task_manager() + .networking_task_source() + .queue(task!(process_favicon_response: move || { + let element = trusted_node.root(); + + if request_generation_id != element.get_request_generation_id() { + // This load is no longer relevant. + return; + }; + + element.process_favicon_response(image); + })); + }) + } + + /// Rasterizes a loaded favicon file if necessary and notifies the embedder about it. + fn process_favicon_response(&self, image: Image) { + // TODO: Include the size attribute here + let window = self.owner_window(); + + let send_rasterized_favicon_to_embedder = |raster_image: &pixels::RasterImage| { + // Let's not worry about animated favicons... + let frame = raster_image.first_frame(); + + let format = match raster_image.format { + PixelFormat::K8 => embedder_traits::PixelFormat::K8, + PixelFormat::KA8 => embedder_traits::PixelFormat::KA8, + PixelFormat::RGB8 => embedder_traits::PixelFormat::RGB8, + PixelFormat::RGBA8 => embedder_traits::PixelFormat::RGBA8, + PixelFormat::BGRA8 => embedder_traits::PixelFormat::BGRA8, + }; + + let embedder_image = embedder_traits::Image::new( + frame.width, + frame.height, + raster_image.bytes.clone(), + raster_image.frames[0].byte_range.clone(), + format, + ); + window.send_to_embedder(EmbedderMsg::NewFavicon(window.webview_id(), embedder_image)); + }; + + match image { + Image::Raster(raster_image) => send_rasterized_favicon_to_embedder(&raster_image), + Image::Vector(vector_image) => { + // This size is completely arbitrary. + let size = DeviceIntSize::new(250, 250); + + let image_cache = window.image_cache(); + if let Some(raster_image) = + image_cache.rasterize_vector_image(vector_image.id, size) + { + send_rasterized_favicon_to_embedder(&raster_image); + } else { + // The rasterization callback will end up calling "process_favicon_response" again, + // but this time with a raster image. + let image_cache_sender = self.register_image_cache_callback(vector_image.id); + image_cache.add_rasterization_complete_listener( + window.pipeline_id(), + vector_image.id, + size, + image_cache_sender, + ); } }, - Err(e) => debug!("Parsing url {} failed: {}", href, e), } } @@ -962,6 +1152,93 @@ fn translate_a_preload_destination(potential_destination: &str) -> Destination { } } +struct FaviconFetchContext { + /// The `` element that caused this fetch operation + link: Trusted, + image_cache: std::sync::Arc, + id: PendingImageId, + + /// The base url of the document that the `` element belongs to. + url: ServoUrl, + + resource_timing: ResourceFetchTiming, +} + +impl FetchResponseListener for FaviconFetchContext { + fn process_request_body(&mut self, _: RequestId) {} + + fn process_request_eof(&mut self, _: RequestId) {} + + fn process_response( + &mut self, + request_id: RequestId, + metadata: Result, + ) { + self.image_cache.notify_pending_response( + self.id, + FetchResponseMsg::ProcessResponse(request_id, metadata.clone()), + ); + } + + fn process_response_chunk(&mut self, request_id: RequestId, chunk: Vec) { + self.image_cache.notify_pending_response( + self.id, + FetchResponseMsg::ProcessResponseChunk(request_id, chunk), + ); + } + + fn process_response_eof( + &mut self, + request_id: RequestId, + response: Result, + ) { + self.image_cache.notify_pending_response( + self.id, + FetchResponseMsg::ProcessResponseEOF(request_id, response), + ); + } + + fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming { + &mut self.resource_timing + } + + fn resource_timing(&self) -> &ResourceFetchTiming { + &self.resource_timing + } + + fn submit_resource_timing(&mut self) { + submit_timing(self, CanGc::note()) + } + + fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec) { + let global = &self.resource_timing_global(); + let link = self.link.root(); + let source_position = link + .upcast::() + .compute_source_position(link.line_number as u32); + global.report_csp_violations(violations, None, Some(source_position)); + } +} + +impl ResourceTimingListener for FaviconFetchContext { + fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) { + ( + InitiatorType::LocalName("link".to_string()), + self.url.clone(), + ) + } + + fn resource_timing_global(&self) -> DomRoot { + self.link.root().upcast::().owner_doc().global() + } +} + +impl PreInvoke for FaviconFetchContext { + fn should_invoke(&self) -> bool { + true + } +} + struct PrefetchContext { /// The `` element that caused this prefetch operation link: Trusted, diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 60564068063..1ab513151f6 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -799,9 +799,9 @@ impl Servo { webview.set_cursor(cursor); } }, - EmbedderMsg::NewFavicon(webview_id, url) => { + EmbedderMsg::NewFavicon(webview_id, image) => { if let Some(webview) = self.get_webview_handle(webview_id) { - webview.set_favicon_url(url.into_url()); + webview.set_favicon(image); } }, EmbedderMsg::NotifyLoadStatusChanged(webview_id, load_status) => { diff --git a/components/servo/webview.rs b/components/servo/webview.rs index bde21fa8bb4..cbf2caf9da9 100644 --- a/components/servo/webview.rs +++ b/components/servo/webview.rs @@ -13,7 +13,7 @@ use compositing_traits::WebViewTrait; use constellation_traits::{EmbedderToConstellationMessage, TraversalDirection}; use dpi::PhysicalSize; use embedder_traits::{ - Cursor, FocusId, InputEvent, JSValue, JavaScriptEvaluationError, LoadStatus, + Cursor, FocusId, Image, InputEvent, JSValue, JavaScriptEvaluationError, LoadStatus, MediaSessionActionType, ScreenGeometry, Theme, TraversalId, ViewportDetails, }; use euclid::{Point2D, Scale, Size2D}; @@ -84,7 +84,7 @@ pub(crate) struct WebViewInner { url: Option, status_text: Option, page_title: Option, - favicon_url: Option, + favicon: Option, focused: bool, animating: bool, cursor: Cursor, @@ -126,7 +126,7 @@ impl WebView { url: None, status_text: None, page_title: None, - favicon_url: None, + favicon: None, focused: false, animating: false, cursor: Cursor::Pointer, @@ -264,21 +264,13 @@ impl WebView { self.delegate().notify_page_title_changed(self, new_value); } - pub fn favicon_url(&self) -> Option { - self.inner().favicon_url.clone() + pub fn favicon(&self) -> Option> { + Ref::filter_map(self.inner(), |inner| inner.favicon.as_ref()).ok() } - pub(crate) fn set_favicon_url(self, new_value: Url) { - if self - .inner() - .favicon_url - .as_ref() - .is_some_and(|url| url == &new_value) - { - return; - } - self.inner_mut().favicon_url = Some(new_value.clone()); - self.delegate().notify_favicon_url_changed(self, new_value); + pub(crate) fn set_favicon(self, new_value: Image) { + self.inner_mut().favicon = Some(new_value); + self.delegate().notify_favicon_changed(self); } pub fn focused(&self) -> bool { diff --git a/components/servo/webview_delegate.rs b/components/servo/webview_delegate.rs index e55c0fb0134..58ff20605f0 100644 --- a/components/servo/webview_delegate.rs +++ b/components/servo/webview_delegate.rs @@ -432,9 +432,9 @@ pub trait WebViewDelegate { /// The [`Cursor`] of the currently loaded page in this [`WebView`] has changed. The new /// cursor can accessed via [`WebView::cursor`]. fn notify_cursor_changed(&self, _webview: WebView, _: Cursor) {} - /// The favicon [`Url`] of the currently loaded page in this [`WebView`] has changed. The new - /// favicon [`Url`] can accessed via [`WebView::favicon_url`]. - fn notify_favicon_url_changed(&self, _webview: WebView, _: Url) {} + /// The favicon of the currently loaded page in this [`WebView`] has changed. The new + /// favicon [`Image`] can accessed via [`WebView::favicon`]. + fn notify_favicon_changed(&self, _webview: WebView) {} /// Notify the embedder that it needs to present a new frame. fn notify_new_frame_ready(&self, _webview: WebView) {} diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index e96a1c8a999..ac500519768 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -17,6 +17,7 @@ use std::collections::HashMap; use std::ffi::c_void; use std::fmt::{Debug, Display, Error, Formatter}; use std::hash::Hash; +use std::ops::Range; use std::path::PathBuf; use std::sync::Arc; @@ -24,7 +25,7 @@ use base::id::{PipelineId, WebViewId}; use crossbeam_channel::Sender; use euclid::{Point2D, Scale, Size2D}; use http::{HeaderMap, Method, StatusCode}; -use ipc_channel::ipc::IpcSender; +use ipc_channel::ipc::{IpcSender, IpcSharedMemory}; use log::warn; use malloc_size_of::malloc_size_of_is_0; use malloc_size_of_derive::MallocSizeOf; @@ -358,6 +359,54 @@ impl TraversalId { } } +#[derive(Clone, Copy, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub enum PixelFormat { + /// Luminance channel only + K8, + /// Luminance + alpha + KA8, + /// RGB, 8 bits per channel + RGB8, + /// RGB + alpha, 8 bits per channel + RGBA8, + /// BGR + alpha, 8 bits per channel + BGRA8, +} + +/// A raster image buffer. +#[derive(Clone, Deserialize, Serialize)] +pub struct Image { + pub width: u32, + pub height: u32, + pub format: PixelFormat, + /// A shared memory block containing the data of one or more image frames. + data: IpcSharedMemory, + range: Range, +} + +impl Image { + pub fn new( + width: u32, + height: u32, + data: IpcSharedMemory, + range: Range, + format: PixelFormat, + ) -> Self { + Self { + width, + height, + format, + data, + range, + } + } + + /// Return the bytes belonging to the first image frame. + pub fn data(&self) -> &[u8] { + &self.data[self.range.clone()] + } +} + #[derive(Deserialize, IntoStaticStr, Serialize)] pub enum EmbedderMsg { /// A status message to be displayed by the browser chrome. @@ -411,7 +460,7 @@ pub enum EmbedderMsg { /// Changes the cursor. SetCursor(WebViewId, Cursor), /// A favicon was detected - NewFavicon(WebViewId, ServoUrl), + NewFavicon(WebViewId, Image), /// The history state has changed. HistoryChanged(WebViewId, Vec, usize), /// A history traversal operation completed. diff --git a/components/url/lib.rs b/components/url/lib.rs index 93906e5cf54..da44a794381 100644 --- a/components/url/lib.rs +++ b/components/url/lib.rs @@ -15,6 +15,7 @@ use std::hash::Hasher; use std::net::IpAddr; use std::ops::{Index, Range, RangeFrom, RangeFull, RangeTo}; use std::path::Path; +use std::str::FromStr; use malloc_size_of_derive::MallocSizeOf; use serde::{Deserialize, Serialize}; @@ -307,3 +308,12 @@ impl From> for ServoUrl { ServoUrl(url) } } + +impl FromStr for ServoUrl { + type Err = ::Err; + + fn from_str(value: &str) -> Result { + let url = Url::from_str(value)?; + Ok(url.into()) + } +} diff --git a/tests/wpt/meta/content-security-policy/img-src/icon-blocked.sub.html.ini b/tests/wpt/meta/content-security-policy/img-src/icon-blocked.sub.html.ini deleted file mode 100644 index 1ed665f5245..00000000000 --- a/tests/wpt/meta/content-security-policy/img-src/icon-blocked.sub.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[icon-blocked.sub.html] - expected: TIMEOUT - [Test that spv event is fired] - expected: NOTRUN diff --git a/tests/wpt/meta/fetch/metadata/generated/element-link-icon.https.sub.html.ini b/tests/wpt/meta/fetch/metadata/generated/element-link-icon.https.sub.html.ini index 413df63e004..ecfb73c57a0 100644 --- a/tests/wpt/meta/fetch/metadata/generated/element-link-icon.https.sub.html.ini +++ b/tests/wpt/meta/fetch/metadata/generated/element-link-icon.https.sub.html.ini @@ -1,67 +1,6 @@ [element-link-icon.https.sub.html] - expected: TIMEOUT - [sec-fetch-site - Same origin no attributes] - expected: TIMEOUT - - [sec-fetch-site - Cross-site no attributes] - expected: NOTRUN - - [sec-fetch-site - Same site no attributes] - expected: NOTRUN - - [sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect no attributes] - expected: NOTRUN - - [sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect no attributes] - expected: NOTRUN - - [sec-fetch-site - Cross-Site -> Same Origin no attributes] - expected: NOTRUN - - [sec-fetch-site - Cross-Site -> Same-Site no attributes] - expected: NOTRUN - - [sec-fetch-site - Cross-Site -> Cross-Site no attributes] - expected: NOTRUN - - [sec-fetch-site - Same-Origin -> Same Origin no attributes] - expected: NOTRUN - - [sec-fetch-site - Same-Origin -> Same-Site no attributes] - expected: NOTRUN - - [sec-fetch-site - Same-Origin -> Cross-Site no attributes] - expected: NOTRUN - - [sec-fetch-site - Same-Site -> Same Origin no attributes] - expected: NOTRUN - - [sec-fetch-site - Same-Site -> Same-Site no attributes] - expected: NOTRUN - - [sec-fetch-site - Same-Site -> Cross-Site no attributes] - expected: NOTRUN - - [sec-fetch-mode no attributes] - expected: NOTRUN - - [sec-fetch-mode attributes: crossorigin] - expected: NOTRUN - - [sec-fetch-mode attributes: crossorigin=anonymous] - expected: NOTRUN - - [sec-fetch-mode attributes: crossorigin=use-credentials] - expected: NOTRUN - [sec-fetch-dest no attributes] - expected: NOTRUN - - [sec-fetch-user no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-storage-access - Cross-site no attributes] - expected: NOTRUN - - [sec-fetch-storage-access - Same site no attributes] - expected: NOTRUN + expected: FAIL diff --git a/tests/wpt/meta/fetch/metadata/generated/element-link-icon.sub.html.ini b/tests/wpt/meta/fetch/metadata/generated/element-link-icon.sub.html.ini index da5d7731a74..d44bd202a00 100644 --- a/tests/wpt/meta/fetch/metadata/generated/element-link-icon.sub.html.ini +++ b/tests/wpt/meta/fetch/metadata/generated/element-link-icon.sub.html.ini @@ -1,55 +1,6 @@ [element-link-icon.sub.html] - expected: TIMEOUT - [sec-fetch-site - Not sent to non-trustworthy same-origin destination no attributes] - expected: TIMEOUT - - [sec-fetch-site - Not sent to non-trustworthy same-site destination no attributes] - expected: NOTRUN - - [sec-fetch-site - Not sent to non-trustworthy cross-site destination no attributes] - expected: NOTRUN - - [sec-fetch-mode - Not sent to non-trustworthy same-origin destination no attributes] - expected: NOTRUN - - [sec-fetch-mode - Not sent to non-trustworthy same-site destination no attributes] - expected: NOTRUN - - [sec-fetch-mode - Not sent to non-trustworthy cross-site destination no attributes] - expected: NOTRUN - - [sec-fetch-dest - Not sent to non-trustworthy same-origin destination no attributes] - expected: NOTRUN - - [sec-fetch-dest - Not sent to non-trustworthy same-site destination no attributes] - expected: NOTRUN - - [sec-fetch-dest - Not sent to non-trustworthy cross-site destination no attributes] - expected: NOTRUN - - [sec-fetch-user - Not sent to non-trustworthy same-origin destination no attributes] - expected: NOTRUN - - [sec-fetch-user - Not sent to non-trustworthy same-site destination no attributes] - expected: NOTRUN - - [sec-fetch-user - Not sent to non-trustworthy cross-site destination no attributes] - expected: NOTRUN - [sec-fetch-site - HTTPS downgrade (header not sent) no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - HTTPS upgrade no attributes] - expected: NOTRUN - - [sec-fetch-site - HTTPS downgrade-upgrade no attributes] - expected: NOTRUN - - [sec-fetch-storage-access - Not sent to non-trustworthy same-origin destination no attributes] - expected: NOTRUN - - [sec-fetch-storage-access - Not sent to non-trustworthy same-site destination no attributes] - expected: NOTRUN - - [sec-fetch-storage-access - Not sent to non-trustworthy cross-site destination no attributes] - expected: NOTRUN + expected: FAIL