diff options
-rw-r--r-- | components/constellation/network_listener.rs | 18 | ||||
-rw-r--r-- | components/net/fetch/methods.rs | 2 | ||||
-rw-r--r-- | components/net/http_loader.rs | 59 | ||||
-rw-r--r-- | components/script/dom/htmllinkelement.rs | 383 | ||||
-rw-r--r-- | components/script/lib.rs | 2 | ||||
-rw-r--r-- | components/script/link_relations.rs | 131 | ||||
-rw-r--r-- | components/shared/net/request.rs | 2 | ||||
-rw-r--r-- | tests/wpt/meta/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html.ini | 66 | ||||
-rw-r--r-- | tests/wpt/meta/fetch/metadata/generated/element-link-prefetch.optional.sub.html.ini | 44 | ||||
-rw-r--r-- | tests/wpt/meta/navigation-timing/prefetch-transfer-size-executor.html.ini | 4 | ||||
-rw-r--r-- | tests/wpt/meta/navigation-timing/prefetch-transfer-size-iframe.html.ini | 4 |
11 files changed, 532 insertions, 183 deletions
diff --git a/components/constellation/network_listener.rs b/components/constellation/network_listener.rs index 9ff19b32648..9afe406b57a 100644 --- a/components/constellation/network_listener.rs +++ b/components/constellation/network_listener.rs @@ -8,12 +8,12 @@ use base::id::PipelineId; use crossbeam_channel::Sender; -use http::HeaderMap; +use http::{header, HeaderMap}; use ipc_channel::ipc; use ipc_channel::router::ROUTER; use log::warn; -use net::http_loader::{set_default_accept, set_default_accept_language}; -use net_traits::request::{Destination, Referrer, RequestBuilder}; +use net::http_loader::{set_default_accept_language, DOCUMENT_ACCEPT_HEADER_VALUE}; +use net_traits::request::{Referrer, RequestBuilder}; use net_traits::response::ResponseInit; use net_traits::{ CoreResourceMsg, FetchChannels, FetchMetadata, FetchResponseMsg, IpcSend, NetworkError, @@ -66,7 +66,17 @@ impl NetworkListener { None, ), None => { - set_default_accept(Destination::Document, &mut listener.request_builder.headers); + if !listener + .request_builder + .headers + .contains_key(header::ACCEPT) + { + listener + .request_builder + .headers + .insert(header::ACCEPT, DOCUMENT_ACCEPT_HEADER_VALUE); + } + set_default_accept_language(&mut listener.request_builder.headers); CoreResourceMsg::Fetch( diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs index 947afbf7060..0a41f930128 100644 --- a/components/net/fetch/methods.rs +++ b/components/net/fetch/methods.rs @@ -133,7 +133,7 @@ pub async fn fetch_with_cors_cache( } // Step 3. - set_default_accept(request.destination, &mut request.headers); + set_default_accept(request); // Step 4. set_default_accept_language(&mut request.headers); diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs index fcf0857dbb5..5e7aa7dfd74 100644 --- a/components/net/http_loader.rs +++ b/components/net/http_loader.rs @@ -35,13 +35,12 @@ use ipc_channel::ipc::{self, IpcSender}; use ipc_channel::router::ROUTER; use log::{debug, error, info, log_enabled, warn}; use net_traits::pub_domains::reg_suffix; -use net_traits::quality::{quality_to_value, Quality, QualityItem}; use net_traits::request::Origin::Origin as SpecificOrigin; use net_traits::request::{ get_cors_unsafe_header_names, is_cors_non_wildcard_request_header_name, is_cors_safelisted_method, is_cors_safelisted_request_header, BodyChunkRequest, - BodyChunkResponse, CacheMode, CredentialsMode, Destination, Origin, RedirectMode, Referrer, - Request, RequestBuilder, RequestMode, ResponseTainting, ServiceWorkersMode, + BodyChunkResponse, CacheMode, CredentialsMode, Destination, Initiator, Origin, RedirectMode, + Referrer, Request, RequestBuilder, RequestMode, ResponseTainting, ServiceWorkersMode, }; use net_traits::response::{HttpsState, Response, ResponseBody, ResponseType}; use net_traits::{ @@ -70,6 +69,10 @@ use crate::hsts::HstsList; use crate::http_cache::{CacheKey, HttpCache}; use crate::resource_thread::AuthCache; +/// <https://fetch.spec.whatwg.org/#document-accept-header-value> +pub const DOCUMENT_ACCEPT_HEADER_VALUE: HeaderValue = + HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); + /// The various states an entry of the HttpCache can be in. #[derive(Clone, Debug, Eq, PartialEq)] pub enum HttpCacheEntryState { @@ -117,41 +120,29 @@ impl Default for HttpState { } } -// Step 3 of https://fetch.spec.whatwg.org/#concept-fetch. -pub fn set_default_accept(destination: Destination, headers: &mut HeaderMap) { - if headers.contains_key(header::ACCEPT) { +/// Step 13 of <https://fetch.spec.whatwg.org/#concept-fetch>. +pub fn set_default_accept(request: &mut Request) { + if request.headers.contains_key(header::ACCEPT) { return; } - let value = match destination { - // Step 3.2. - Destination::Document => vec![ - QualityItem::new(mime::TEXT_HTML, Quality::from_u16(1000)), - QualityItem::new( - "application/xhtml+xml".parse().unwrap(), - Quality::from_u16(1000), - ), - QualityItem::new("application/xml".parse().unwrap(), Quality::from_u16(900)), - QualityItem::new(mime::STAR_STAR, Quality::from_u16(800)), - ], - // Step 3.3. - Destination::Image => vec![ - QualityItem::new(mime::IMAGE_PNG, Quality::from_u16(1000)), - QualityItem::new(mime::IMAGE_SVG, Quality::from_u16(1000)), - QualityItem::new(mime::IMAGE_STAR, Quality::from_u16(800)), - QualityItem::new(mime::STAR_STAR, Quality::from_u16(500)), - ], - // Step 3.3. - Destination::Style => vec![ - QualityItem::new(mime::TEXT_CSS, Quality::from_u16(1000)), - QualityItem::new(mime::STAR_STAR, Quality::from_u16(100)), - ], - // Step 3.1. - _ => vec![QualityItem::new(mime::STAR_STAR, Quality::from_u16(1000))], + + let value = if request.initiator == Initiator::Prefetch { + DOCUMENT_ACCEPT_HEADER_VALUE + } else { + match request.destination { + Destination::Document | Destination::Frame | Destination::IFrame => { + DOCUMENT_ACCEPT_HEADER_VALUE + }, + Destination::Image => { + HeaderValue::from_static("image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5") + }, + Destination::Json => HeaderValue::from_static("application/json,*/*;q=0.5"), + Destination::Style => HeaderValue::from_static("text/css,*/*;q=0.1"), + _ => HeaderValue::from_static("*/*"), + } }; - // Step 3.4. - // TODO(eijebong): Change this once typed headers are done - headers.insert(header::ACCEPT, quality_to_value(value)); + request.headers.insert(header::ACCEPT, value); } fn set_default_accept_encoding(headers: &mut HeaderMap) { diff --git a/components/script/dom/htmllinkelement.rs b/components/script/dom/htmllinkelement.rs index fef207d4a2e..825c01b4f9a 100644 --- a/components/script/dom/htmllinkelement.rs +++ b/components/script/dom/htmllinkelement.rs @@ -2,46 +2,61 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::borrow::ToOwned; +use std::borrow::{Borrow, ToOwned}; use std::cell::Cell; use std::default::Default; +use std::sync; use cssparser::{Parser as CssParser, ParserInput}; use dom_struct::dom_struct; use embedder_traits::EmbedderMsg; use html5ever::{local_name, namespace_url, ns, LocalName, Prefix}; +use ipc_channel::ipc; +use ipc_channel::router::ROUTER; use js::rust::HandleObject; -use net_traits::ReferrerPolicy; +use net_traits::request::{CorsSettings, Destination, Initiator, Referrer, RequestBuilder}; +use net_traits::{ + CoreResourceMsg, FetchChannels, FetchMetadata, FetchResponseListener, IpcSend, NetworkError, + ReferrerPolicy, ResourceFetchTiming, ResourceTimingType, +}; use servo_arc::Arc; use servo_atoms::Atom; +use servo_url::ServoUrl; use style::attr::AttrValue; use style::media_queries::MediaList; use style::parser::ParserContext as CssParserContext; -use style::str::HTML_SPACE_CHARACTERS; use style::stylesheets::{CssRuleType, Origin, Stylesheet, UrlExtraData}; use style_traits::ParsingMode; +use super::types::{EventTarget, GlobalScope}; use crate::dom::attr::Attr; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::DOMTokenListBinding::DOMTokenList_Binding::DOMTokenListMethods; use crate::dom::bindings::codegen::Bindings::HTMLLinkElementBinding::HTMLLinkElementMethods; use crate::dom::bindings::inheritance::Castable; +use crate::dom::bindings::refcounted::Trusted; +use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::root::{DomRoot, MutNullableDom}; use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::cssstylesheet::CSSStyleSheet; use crate::dom::document::Document; use crate::dom::domtokenlist::DOMTokenList; use crate::dom::element::{ - cors_setting_for_element, reflect_cross_origin_attribute, reflect_referrer_policy_attribute, - set_cross_origin_attribute, AttributeMutation, Element, ElementCreator, + cors_setting_for_element, referrer_policy_for_element, reflect_cross_origin_attribute, + reflect_referrer_policy_attribute, set_cross_origin_attribute, AttributeMutation, Element, + ElementCreator, }; use crate::dom::htmlelement::HTMLElement; use crate::dom::node::{ document_from_node, stylesheets_owner_from_node, window_from_node, BindContext, Node, UnbindContext, }; +use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::stylesheet::StyleSheet as DOMStyleSheet; use crate::dom::virtualmethods::VirtualMethods; +use crate::fetch::create_a_potential_cors_request; +use crate::link_relations::LinkRelations; +use crate::network_listener::{submit_timing, NetworkListener, PreInvoke, ResourceTimingListener}; use crate::stylesheet_loader::{StylesheetContextSource, StylesheetLoader, StylesheetOwner}; #[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)] @@ -53,10 +68,33 @@ impl RequestGenerationId { } } +/// <https://html.spec.whatwg.org/multipage/#link-processing-options> +struct LinkProcessingOptions { + href: String, + destination: Option<Destination>, + integrity: String, + link_type: String, + cross_origin: Option<CorsSettings>, + referrer_policy: Option<ReferrerPolicy>, + source_set: Option<()>, + base_url: ServoUrl, + // Some fields that we don't need yet are missing +} + #[dom_struct] pub struct HTMLLinkElement { htmlelement: HTMLElement, + /// The relations as specified by the "rel" attribute rel_list: MutNullableDom<DOMTokenList>, + + /// The link relations as they are used in practice. + /// + /// The reason this is seperate from [HTMLLinkElement::rel_list] is that + /// a literal list is a bit unwieldy and that there are corner cases to consider + /// (Like `rev="made"` implying an author relationship that is not represented in rel_list) + #[no_trace] + relations: Cell<LinkRelations>, + #[ignore_malloc_size_of = "Arc"] #[no_trace] stylesheet: DomRefCell<Option<Arc<Stylesheet>>>, @@ -83,6 +121,7 @@ impl HTMLLinkElement { HTMLLinkElement { htmlelement: HTMLElement::new_inherited(local_name, prefix, document), rel_list: Default::default(), + relations: Cell::new(LinkRelations::empty()), parser_inserted: Cell::new(creator.is_parser_created()), stylesheet: DomRefCell::new(None), cssom_stylesheet: MutNullableDom::new(None), @@ -146,13 +185,7 @@ impl HTMLLinkElement { } pub fn is_alternate(&self) -> bool { - let rel = get_attr(self.upcast(), &local_name!("rel")); - match rel { - Some(ref value) => value - .split(HTML_SPACE_CHARACTERS) - .any(|s| s.eq_ignore_ascii_case("alternate")), - None => false, - } + self.relations.get().contains(LinkRelations::ALTERNATE) } fn clean_stylesheet_ownership(&self) { @@ -171,27 +204,6 @@ fn get_attr(element: &Element, local_name: &LocalName) -> Option<String> { }) } -fn string_is_stylesheet(value: &Option<String>) -> bool { - match *value { - Some(ref value) => value - .split(HTML_SPACE_CHARACTERS) - .any(|s| s.eq_ignore_ascii_case("stylesheet")), - None => false, - } -} - -/// Favicon spec usage in accordance with CEF implementation: -/// only url of icon is required/used -/// <https://html.spec.whatwg.org/multipage/#rel-icon> -fn is_favicon(value: &Option<String>) -> bool { - match *value { - Some(ref value) => value - .split(HTML_SPACE_CHARACTERS) - .any(|s| s.eq_ignore_ascii_case("icon") || s.eq_ignore_ascii_case("apple-touch-icon")), - None => false, - } -} - impl VirtualMethods for HTMLLinkElement { fn super_type(&self) -> Option<&dyn VirtualMethods> { Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods) @@ -203,25 +215,33 @@ impl VirtualMethods for HTMLLinkElement { return; } - let rel = get_attr(self.upcast(), &local_name!("rel")); match *attr.local_name() { + local_name!("rel") | local_name!("rev") => { + self.relations + .set(LinkRelations::for_element(self.upcast())); + }, local_name!("href") => { - if string_is_stylesheet(&rel) { + if self.relations.get().contains(LinkRelations::STYLESHEET) { self.handle_stylesheet_url(&attr.value()); - } else if is_favicon(&rel) { + } + + if self.relations.get().contains(LinkRelations::ICON) { let sizes = get_attr(self.upcast(), &local_name!("sizes")); - self.handle_favicon_url(rel.as_ref().unwrap(), &attr.value(), &sizes); + self.handle_favicon_url(&attr.value(), &sizes); + } + + if self.relations.get().contains(LinkRelations::PREFETCH) { + self.fetch_and_process_prefetch_link(&attr.value()); } }, - local_name!("sizes") => { - if is_favicon(&rel) { - if let Some(ref href) = get_attr(self.upcast(), &local_name!("href")) { - self.handle_favicon_url( - rel.as_ref().unwrap(), - href, - &Some(attr.value().to_string()), - ); - } + 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())); + } + }, + local_name!("crossorigin") => { + if self.relations.get().contains(LinkRelations::PREFETCH) { + self.fetch_and_process_prefetch_link(&attr.value()); } }, _ => {}, @@ -243,21 +263,26 @@ impl VirtualMethods for HTMLLinkElement { s.bind_to_tree(context); } + self.relations + .set(LinkRelations::for_element(self.upcast())); + if context.tree_connected { let element = self.upcast(); - let rel = get_attr(element, &local_name!("rel")); - let href = get_attr(element, &local_name!("href")); - let sizes = get_attr(self.upcast(), &local_name!("sizes")); - - match href { - Some(ref href) if string_is_stylesheet(&rel) => { - self.handle_stylesheet_url(href); - }, - Some(ref href) if is_favicon(&rel) => { - self.handle_favicon_url(rel.as_ref().unwrap(), href, &sizes); - }, - _ => {}, + if let Some(href) = get_attr(element, &local_name!("href")) { + let relations = self.relations.get(); + if relations.contains(LinkRelations::STYLESHEET) { + self.handle_stylesheet_url(&href); + } + + if relations.contains(LinkRelations::ICON) { + let sizes = get_attr(self.upcast(), &local_name!("sizes")); + self.handle_favicon_url(&href, &sizes); + } + + if relations.contains(LinkRelations::PREFETCH) { + self.fetch_and_process_prefetch_link(&href); + } } } } @@ -275,6 +300,118 @@ impl VirtualMethods for HTMLLinkElement { } impl HTMLLinkElement { + /// <https://html.spec.whatwg.org/multipage/#create-link-options-from-element> + fn processing_options(&self) -> LinkProcessingOptions { + let element = self.upcast::<Element>(); + + // Step 1. Let document be el's node document. + let document = self.upcast::<Node>().owner_doc(); + + // Step 2. Let options be a new link processing options + let destination = element + .get_attribute(&ns!(), &local_name!("as")) + .map(|attr| translate_a_preload_destination(&*attr.value())) + .unwrap_or(Destination::None); + + let mut options = LinkProcessingOptions { + href: String::new(), + destination: Some(destination), + integrity: String::new(), + link_type: String::new(), + cross_origin: cors_setting_for_element(element), + referrer_policy: referrer_policy_for_element(element), + source_set: None, // FIXME + base_url: document.borrow().base_url(), + }; + + // Step 3. If el has an href attribute, then set options's href to the value of el's href attribute. + if let Some(href_attribute) = element.get_attribute(&ns!(), &local_name!("href")) { + options.href = (**href_attribute.value()).to_owned(); + } + + // Step 4. If el has an integrity attribute, then set options's integrity + // to the value of el's integrity content attribute. + if let Some(integrity_attribute) = element.get_attribute(&ns!(), &local_name!("integrity")) + { + options.integrity = (**integrity_attribute.value()).to_owned(); + } + + // Step 5. If el has a type attribute, then set options's type to the value of el's type attribute. + if let Some(type_attribute) = element.get_attribute(&ns!(), &local_name!("type")) { + options.link_type = (**type_attribute.value()).to_owned(); + } + + // Step 6. Assert: options's href is not the empty string, or options's source set is not null. + assert!(!options.href.is_empty() || options.source_set.is_some()); + + // Step 7. Return options. + options + } + + /// 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. + if href.is_empty() { + return; + } + + // Step 2. Let options be the result of creating link options from el. + let mut options = self.processing_options(); + + // Step 3. Set options's destination to the empty string. + options.destination = Some(Destination::None); + + // Step 4. Let request be the result of creating a link request given options. + let url = options.base_url.clone(); + let Some(request) = options.create_link_request() else { + // Step 5. If request is null, then return. + return; + }; + + // Step 6. Set request's initiator to "prefetch". + let request = request.initiator(Initiator::Prefetch); + + // (Step 7, firing load/error events is handled in the FetchResponseListener impl for PrefetchContext) + + // Step 8. The user agent should fetch request, with processResponseConsumeBody set to processPrefetchResponse. + let (action_sender, action_receiver) = ipc::channel().unwrap(); + let document = self.upcast::<Node>().owner_doc(); + let window = document.window(); + + let (task_source, canceller) = window + .task_manager() + .networking_task_source_with_canceller(); + + let fetch_context = sync::Arc::new(sync::Mutex::new(PrefetchContext { + url: url, + link: Trusted::new(self), + resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource), + })); + + let listener = NetworkListener { + context: fetch_context, + task_source, + canceller: Some(canceller), + }; + + ROUTER.add_route( + action_receiver.to_opaque(), + Box::new(move |message| { + listener.notify_fetch(message.to().unwrap()); + }), + ); + + window + .upcast::<GlobalScope>() + .resource_threads() + .sender() + .send(CoreResourceMsg::Fetch( + request, + FetchChannels::ResponseMsg(action_sender, None), + )) + .unwrap(); + } + /// <https://html.spec.whatwg.org/multipage/#concept-link-obtain> fn handle_stylesheet_url(&self, href: &str) { let document = document_from_node(self); @@ -348,7 +485,7 @@ impl HTMLLinkElement { ); } - fn handle_favicon_url(&self, _rel: &str, href: &str, _sizes: &Option<String>) { + fn handle_favicon_url(&self, href: &str, _sizes: &Option<String>) { let document = document_from_node(self); match document.base_url().join(href) { Ok(url) => { @@ -511,3 +648,131 @@ impl HTMLLinkElementMethods for HTMLLinkElement { self.get_cssom_stylesheet().map(DomRoot::upcast) } } + +impl LinkProcessingOptions { + /// <https://html.spec.whatwg.org/multipage/#create-a-link-request> + fn create_link_request(self) -> Option<RequestBuilder> { + // Step 1. Assert: options's href is not the empty string. + assert!(!self.href.is_empty()); + + // Step 2. If options's destination is null, then return null. + let Some(destination) = self.destination else { + return None; + }; + + // Step 3. Let url be the result of encoding-parsing a URL given options's href, relative to options's base URL. + // TODO: The spec passes a base url which is incompatible with the + // "encoding-parse a URL" algorithm. + let Ok(url) = self.base_url.join(&self.href) else { + // Step 4. If url is failure, then return null. + return None; + }; + + // Step 5. Let request be the result of creating a potential-CORS request given + // url, options's destination, and options's crossorigin. + // FIXME: Step 6. Set request's policy container to options's policy container. + // Step 7. Set request's integrity metadata to options's integrity. + // FIXME: Step 8. Set request's cryptographic nonce metadata to options's cryptographic nonce metadata. + // Step 9. Set request's referrer policy to options's referrer policy. + // FIXME: Step 10. Set request's client to options's environment. + // FIXME: Step 11. Set request's priority to options's fetch priority. + // FIXME: Use correct referrer + let builder = create_a_potential_cors_request( + url, + destination, + self.cross_origin, + None, + Referrer::NoReferrer, + ) + .integrity_metadata(self.integrity) + .referrer_policy(self.referrer_policy); + + // Step 12. Return request. + Some(builder) + } +} + +/// <https://html.spec.whatwg.org/multipage/#translate-a-preload-destination> +fn translate_a_preload_destination(potential_destination: &str) -> Destination { + match potential_destination { + "fetch" => Destination::None, + "font" => Destination::Font, + "image" => Destination::Image, + "script" => Destination::Script, + "track" => Destination::Track, + _ => Destination::None, + } +} + +struct PrefetchContext { + /// The `<link>` element that caused this prefetch operation + link: Trusted<HTMLLinkElement>, + + resource_timing: ResourceFetchTiming, + + /// The url being prefetched + url: ServoUrl, +} + +impl FetchResponseListener for PrefetchContext { + fn process_request_body(&mut self) {} + + fn process_request_eof(&mut self) {} + + fn process_response(&mut self, fetch_metadata: Result<FetchMetadata, NetworkError>) { + _ = fetch_metadata; + } + + fn process_response_chunk(&mut self, chunk: Vec<u8>) { + _ = chunk; + } + + // Step 7 of `fetch and process the linked resource` in https://html.spec.whatwg.org/multipage/#link-type-prefetch + fn process_response_eof(&mut self, response: Result<ResourceFetchTiming, NetworkError>) { + if response.is_err() { + // Step 1. If response is a network error, fire an event named error at el. + self.link + .root() + .upcast::<EventTarget>() + .fire_event(atom!("error")); + } else { + // Step 2. Otherwise, fire an event named load at el. + self.link + .root() + .upcast::<EventTarget>() + .fire_event(atom!("load")); + } + } + + 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) + } +} + +impl ResourceTimingListener for PrefetchContext { + fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) { + ( + InitiatorType::LocalName("prefetch".to_string()), + self.url.clone(), + ) + } + + fn resource_timing_global(&self) -> DomRoot<GlobalScope> { + self.link.root().upcast::<Node>().owner_doc().global() + } +} + +impl PreInvoke for PrefetchContext { + fn should_invoke(&self) -> bool { + // Prefetch requests are never aborted. + true + } +} diff --git a/components/script/lib.rs b/components/script/lib.rs index 534e1917194..1db1a94730b 100644 --- a/components/script/lib.rs +++ b/components/script/lib.rs @@ -94,6 +94,8 @@ mod webdriver_handlers; #[warn(deprecated)] mod window_named_properties; +mod link_relations; + pub use init::init; pub use script_runtime::JSEngineSetup; diff --git a/components/script/link_relations.rs b/components/script/link_relations.rs new file mode 100644 index 00000000000..d32ea9e4e92 --- /dev/null +++ b/components/script/link_relations.rs @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use html5ever::{local_name, namespace_url, ns}; +use malloc_size_of::malloc_size_of_is_0; +use style::str::HTML_SPACE_CHARACTERS; + +use crate::dom::types::Element; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug)] + pub struct LinkRelations: u32 { + const ALTERNATE = 1; + const AUTHOR = 1 << 1; + const BOOKMARK = 1 << 2; + const CANONICAL = 1 << 3; + const DNS_PREFETCH = 1 << 4; + const EXPECT = 1 << 5; + const EXTERNAL = 1 << 6; + const HELP = 1 << 7; + const ICON = 1 << 8; + const LICENSE = 1 << 9; + const NEXT = 1 << 10; + const MANIFEST = 1 << 11; + const MODULE_PRELOAD = 1 << 12; + const NO_FOLLOW = 1 << 13; + const NO_OPENER = 1 << 14; + const NO_REFERRER = 1 << 15; + const OPENER = 1 << 16; + const PING_BACK = 1 << 17; + const PRECONNECT = 1 << 18; + const PREFETCH = 1 << 19; + const PRELOAD = 1 << 20; + const PREV = 1 << 21; + const PRIVACY_POLICY = 1 << 22; + const SEARCH = 1 << 23; + const STYLESHEET = 1 << 24; + const TAG = 1 << 25; + const TermsOfService = 1 << 26; + } +} + +impl LinkRelations { + pub fn for_element(element: &Element) -> Self { + let rel = element.get_attribute(&ns!(), &local_name!("rel")).map(|e| { + let value = e.value(); + (**value).to_owned() + }); + + // FIXME: for a, area and form elements we need to allow a different + // set of attributes + let mut relations = rel + .map(|attribute| { + attribute + .split(HTML_SPACE_CHARACTERS) + .map(Self::from_single_keyword_for_link_element) + .collect() + }) + .unwrap_or(Self::empty()); + + // For historical reasons, "rev=made" is treated as if the "author" relation was specified + let has_legacy_author_relation = element + .get_attribute(&ns!(), &local_name!("rev")) + .is_some_and(|rev| &**rev.value() == "made"); + if has_legacy_author_relation { + relations |= Self::AUTHOR; + } + + relations + } + + /// Parse one of the relations allowed for the `<link>` element + /// + /// If the keyword is invalid then `Self::empty` is returned. + fn from_single_keyword_for_link_element(keyword: &str) -> Self { + if keyword.eq_ignore_ascii_case("alternate") { + Self::ALTERNATE + } else if keyword.eq_ignore_ascii_case("canonical") { + Self::CANONICAL + } else if keyword.eq_ignore_ascii_case("author") { + Self::AUTHOR + } else if keyword.eq_ignore_ascii_case("dns-prefetch") { + Self::DNS_PREFETCH + } else if keyword.eq_ignore_ascii_case("expect") { + Self::EXPECT + } else if keyword.eq_ignore_ascii_case("help") { + Self::HELP + } else if keyword.eq_ignore_ascii_case("icon") || + keyword.eq_ignore_ascii_case("shortcut icon") || + keyword.eq_ignore_ascii_case("apple-touch-icon") + { + // TODO: "apple-touch-icon" is not in the spec. Where did it come from? Do we need it? + // There is also "apple-touch-icon-precomposed" listed in + // https://github.com/servo/servo/blob/e43e4778421be8ea30db9d5c553780c042161522/components/script/dom/htmllinkelement.rs#L452-L467 + Self::ICON + } else if keyword.eq_ignore_ascii_case("manifest") { + Self::MANIFEST + } else if keyword.eq_ignore_ascii_case("modulepreload") { + Self::MODULE_PRELOAD + } else if keyword.eq_ignore_ascii_case("license") || + keyword.eq_ignore_ascii_case("copyright") + { + Self::LICENSE + } else if keyword.eq_ignore_ascii_case("next") { + Self::NEXT + } else if keyword.eq_ignore_ascii_case("pingback") { + Self::PING_BACK + } else if keyword.eq_ignore_ascii_case("preconnect") { + Self::PRECONNECT + } else if keyword.eq_ignore_ascii_case("prefetch") { + Self::PREFETCH + } else if keyword.eq_ignore_ascii_case("preload") { + Self::PRELOAD + } else if keyword.eq_ignore_ascii_case("prev") || keyword.eq_ignore_ascii_case("previous") { + Self::PREV + } else if keyword.eq_ignore_ascii_case("privacy-policy") { + Self::PRIVACY_POLICY + } else if keyword.eq_ignore_ascii_case("search") { + Self::SEARCH + } else if keyword.eq_ignore_ascii_case("stylesheet") { + Self::STYLESHEET + } else if keyword.eq_ignore_ascii_case("terms-of-service") { + Self::TermsOfService + } else { + Self::empty() + } + } +} + +malloc_size_of_is_0!(LinkRelations); diff --git a/components/shared/net/request.rs b/components/shared/net/request.rs index 2c644d9ba54..b87351c7dc1 100644 --- a/components/shared/net/request.rs +++ b/components/shared/net/request.rs @@ -25,6 +25,8 @@ pub enum Initiator { ImageSet, Manifest, XSLT, + Prefetch, + Link, } /// A request [destination](https://fetch.spec.whatwg.org/#concept-request-destination) diff --git a/tests/wpt/meta/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html.ini b/tests/wpt/meta/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html.ini index 3bcfbe3081b..f083672c134 100644 --- a/tests/wpt/meta/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html.ini +++ b/tests/wpt/meta/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html.ini @@ -1,97 +1,93 @@ [element-link-prefetch.https.optional.sub.html] - expected: TIMEOUT [sec-fetch-site - Same origin no attributes] - expected: TIMEOUT + expected: FAIL [sec-fetch-site - Cross-site no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - Same site no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - Cross-Site -> Same Origin no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - Cross-Site -> Same-Site no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - Cross-Site -> Cross-Site no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - Same-Origin -> Same Origin no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - Same-Origin -> Same-Site no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - Same-Origin -> Cross-Site no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - Same-Site -> Same Origin no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - Same-Site -> Same-Site no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - Same-Site -> Cross-Site no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-mode no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-mode attributes: crossorigin] - expected: NOTRUN + expected: FAIL [sec-fetch-mode attributes: crossorigin=anonymous] - expected: NOTRUN + expected: FAIL [sec-fetch-mode attributes: crossorigin=use-credentials] - expected: NOTRUN + expected: FAIL [sec-fetch-dest no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-dest attributes: as=audio] - expected: NOTRUN + expected: FAIL [sec-fetch-dest attributes: as=document] - expected: NOTRUN + expected: FAIL [sec-fetch-dest attributes: as=embed] - expected: NOTRUN + expected: FAIL [sec-fetch-dest attributes: as=fetch] - expected: NOTRUN + expected: FAIL [sec-fetch-dest attributes: as=font] - expected: NOTRUN + expected: FAIL [sec-fetch-dest attributes: as=image] - expected: NOTRUN + expected: FAIL [sec-fetch-dest attributes: as=object] - expected: NOTRUN + expected: FAIL [sec-fetch-dest attributes: as=script] - expected: NOTRUN + expected: FAIL [sec-fetch-dest attributes: as=style] - expected: NOTRUN + expected: FAIL [sec-fetch-dest attributes: as=track] - expected: NOTRUN + expected: FAIL [sec-fetch-dest attributes: as=video] - expected: NOTRUN + expected: FAIL [sec-fetch-dest attributes: as=worker] - expected: NOTRUN - - [sec-fetch-user no attributes] - expected: NOTRUN + expected: FAIL diff --git a/tests/wpt/meta/fetch/metadata/generated/element-link-prefetch.optional.sub.html.ini b/tests/wpt/meta/fetch/metadata/generated/element-link-prefetch.optional.sub.html.ini index dbde77c8aa8..c91344a198e 100644 --- a/tests/wpt/meta/fetch/metadata/generated/element-link-prefetch.optional.sub.html.ini +++ b/tests/wpt/meta/fetch/metadata/generated/element-link-prefetch.optional.sub.html.ini @@ -1,46 +1,6 @@ [element-link-prefetch.optional.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 - [sec-fetch-site - HTTPS upgrade no attributes] - expected: NOTRUN + expected: FAIL [sec-fetch-site - HTTPS downgrade-upgrade no attributes] - expected: NOTRUN + expected: FAIL diff --git a/tests/wpt/meta/navigation-timing/prefetch-transfer-size-executor.html.ini b/tests/wpt/meta/navigation-timing/prefetch-transfer-size-executor.html.ini deleted file mode 100644 index f9951abc0d2..00000000000 --- a/tests/wpt/meta/navigation-timing/prefetch-transfer-size-executor.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[prefetch-transfer-size-executor.html] - expected: TIMEOUT - [Navigation timing transfer size for a prefetched navigation should be 0.] - expected: TIMEOUT diff --git a/tests/wpt/meta/navigation-timing/prefetch-transfer-size-iframe.html.ini b/tests/wpt/meta/navigation-timing/prefetch-transfer-size-iframe.html.ini deleted file mode 100644 index 96e2de6a9f2..00000000000 --- a/tests/wpt/meta/navigation-timing/prefetch-transfer-size-iframe.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[prefetch-transfer-size-iframe.html] - expected: TIMEOUT - [Navigation timing transfer size for a prefetched navigation should be 0.] - expected: TIMEOUT |